From 77afe7890db12dfdfef524666e58d450d0fa712b Mon Sep 17 00:00:00 2001 From: sobolevn Date: Fri, 3 Nov 2023 21:28:24 +0300 Subject: [PATCH 1/3] gh-111495: Add `PyFile_*` CAPI tests --- Lib/test/test_capi/test_file.py | 142 ++++++++++++++++++++++++++++++++ Modules/_testcapi/file.c | 84 ++++++++++++++++++- 2 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 Lib/test/test_capi/test_file.py diff --git a/Lib/test/test_capi/test_file.py b/Lib/test/test_capi/test_file.py new file mode 100644 index 00000000000000..0bfa2d7016f206 --- /dev/null +++ b/Lib/test/test_capi/test_file.py @@ -0,0 +1,142 @@ +import unittest +import io +import os + +from test.support import import_helper, os_helper + +# Skip this test if the _testcapi module isn't available. +_testcapi = import_helper.import_module('_testcapi') + +NULL = None + + +class TestPyFileCAPI(unittest.TestCase): + def tearDown(self): + try: + os.unlink(os_helper.TESTFN) + except FileNotFoundError: + pass + + def test_file_from_fd(self): + # PyFile_FromFd() + with open(os_helper.TESTFN, "w") as f: + # raise ValueError(*file_mode, 0, "utf-8", "strict", "\n", 0,) + file_obj = _testcapi.file_from_fd( + f.fileno(), os_helper.TESTFN, "w", + 1, "utf-8", "strict", "\n", 0, + ) + + # We don't apply heavy testing here, because inside it directly calls + # `_io.open` which is fully tested in `test_io`. + self.assertIsInstance(file_obj, io.TextIOWrapper) + + def test_file_get_line(self): + # PyFile_GetLine + get_line = _testcapi.file_get_line + + # Create file with unicode content: + first_line = "text with юникод 统一码" + with open(os_helper.TESTFN, "w") as f: + f.writelines([first_line]) + + with open(os_helper.TESTFN) as f: + self.assertEqual(get_line(f, 0), first_line) + with open(os_helper.TESTFN) as f: + self.assertEqual(get_line(f, 4), 'text') + with open(os_helper.TESTFN) as f: + self.assertEqual(get_line(f, -1), first_line) + + # Create file with bytes content: + first_line = "text with юникод 统一码".encode("utf8") + with open(os_helper.TESTFN, mode="wb") as f: + f.writelines([first_line]) + + with open(os_helper.TESTFN, 'rb') as f: + self.assertEqual(get_line(f, 0), first_line) + with open(os_helper.TESTFN, 'rb') as f: + self.assertEqual(get_line(f, 4), b'text') + with open(os_helper.TESTFN, 'rb') as f: + self.assertEqual(get_line(f, -1), first_line) + + # Create empty file: + with open(os_helper.TESTFN, mode="w") as f: + pass + + with open(os_helper.TESTFN) as f: + self.assertEqual(get_line(f, 0), '') + with open(os_helper.TESTFN) as f: + self.assertEqual(get_line(f, 4), '') + with open(os_helper.TESTFN) as f: + self.assertRaises(EOFError, get_line, f, -1) + + # Not `bytes` or `str` is returned: + class _BadIO(io.IOBase): + def readline(self, size = 0): + return object() + + self.assertRaises(TypeError, get_line, _BadIO(), 0) + self.assertRaises(AttributeError, get_line, object(), 0) + # CRASHES: get_line(NULL) + + def test_file_write_object(self): + # PyFile_WriteObject + write = _testcapi.file_write_object + + def write_and_return(obj, flags=0): + dst = io.StringIO() + write(obj, dst, flags) + return dst.getvalue() + + self.assertEqual(write_and_return(1), '1') + self.assertEqual(write_and_return('1'), "'1'") + self.assertEqual(write_and_return(False), 'False') + self.assertEqual(write_and_return(b'123'), "b'123'") + + class Custom: + def __str__(self): + return '' + def __repr__(self): + return '' + + self.assertEqual(write_and_return(Custom()), '') + self.assertEqual( + write_and_return(Custom(), flags=_testcapi.Py_PRINT_RAW), + '', + ) + + self.assertRaises(TypeError, write, object(), None) + # CRASHES: write(NULL, io.StringIO(), flags) + # CRASHES: write(NULL, NULL, flags) + + def test_file_write_string(self): + # PyFile_WriteString + write = _testcapi.file_write_string + + def write_and_return(string): + dst = io.StringIO() + write(string, dst) + return dst.getvalue() + + self.assertEqual(write_and_return("abc"), "abc") + self.assertEqual( + write_and_return("text with юникод 统一码"), + "text with юникод 统一码", + ) + self.assertRaises(TypeError, write, object(), io.StringIO()) + self.assertRaises(AttributeError, write, 'abc', object()) + # CRASHES: write('', NULL) + + def test_object_as_file_descriptor(self): + # PyObject_AsFileDescriptor() + as_fd = _testcapi.object_as_file_descriptor + with open(os_helper.TESTFN, mode="w") as f: + self.assertEqual(as_fd(f), f.fileno()) + with open(os_helper.TESTFN, mode="w") as f: + self.assertEqual(as_fd(f.fileno()), f.fileno()) + + class _BadIO(io.IOBase): + def fileno(self): + return object() # not int + self.assertRaises(TypeError, as_fd, _BadIO()) + self.assertRaises(TypeError, as_fd, object()) + # CRASHES as_fd(NULL) diff --git a/Modules/_testcapi/file.c b/Modules/_testcapi/file.c index 634563f6ea12cb..87cbd1128ac4e9 100644 --- a/Modules/_testcapi/file.c +++ b/Modules/_testcapi/file.c @@ -1,15 +1,97 @@ #include "parts.h" #include "util.h" +static PyObject * +file_from_fd(PyObject *self, PyObject *args) +{ + int fd; + const char *name; + const char *mode; + int buffering; + const char *encoding; + const char *errors; + const char *newline; + int closefd; + if (!PyArg_ParseTuple(args, "ississsi", + &fd, + &name, &mode, + &buffering, + &encoding, &errors, &newline, + &closefd)) { + return NULL; + } + + return PyFile_FromFd(fd, + name, mode, + buffering, + encoding, errors, newline, + closefd); +} + +static PyObject * +file_get_line(PyObject *self, PyObject *args) +{ + PyObject *obj; + int n; + if (!PyArg_ParseTuple(args, "Oi", &obj, &n)) { + return NULL; + } + + NULLABLE(obj); + return PyFile_GetLine(obj, n); +} + +static PyObject * +file_write_object(PyObject *self, PyObject *args) +{ + PyObject *obj, *p; + int flags; + if (!PyArg_ParseTuple(args, "OOi", &obj, &p, &flags)) { + return NULL; + } + + NULLABLE(obj); + NULLABLE(p); + RETURN_INT(PyFile_WriteObject(obj, p, flags)); +} + +static PyObject * +file_write_string(PyObject *self, PyObject *args) +{ + const char *string; + PyObject *f; + if (!PyArg_ParseTuple(args, "sO", &string, &f)) { + return NULL; + } + + NULLABLE(f); + RETURN_INT(PyFile_WriteString(string, f)); +} + +static PyObject * +object_as_file_descriptor(PyObject *self, PyObject *obj) +{ + NULLABLE(obj); + RETURN_INT(PyObject_AsFileDescriptor(obj)); +} static PyMethodDef test_methods[] = { + {"file_from_fd", file_from_fd, METH_VARARGS}, + {"file_get_line", file_get_line, METH_VARARGS}, + {"file_write_object", file_write_object, METH_VARARGS}, + {"file_write_string", file_write_string, METH_VARARGS}, + {"object_as_file_descriptor", object_as_file_descriptor, METH_O}, {NULL}, }; int _PyTestCapi_Init_File(PyObject *m) { - if (PyModule_AddFunctions(m, test_methods) < 0){ + if (PyModule_AddFunctions(m, test_methods) < 0) { + return -1; + } + + if (PyModule_AddIntMacro(m, Py_PRINT_RAW) < 0) { return -1; } From 52c5918b494c0c49fed8e5cb937334fde8a4778e Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sun, 5 Nov 2023 17:23:49 +0300 Subject: [PATCH 2/3] Address review --- Lib/test/test_capi/test_file.py | 318 ++++++++++++++++++++++++-------- Modules/_testcapi/file.c | 14 +- 2 files changed, 243 insertions(+), 89 deletions(-) diff --git a/Lib/test/test_capi/test_file.py b/Lib/test/test_capi/test_file.py index 0bfa2d7016f206..89d2043e7ca51c 100644 --- a/Lib/test/test_capi/test_file.py +++ b/Lib/test/test_capi/test_file.py @@ -10,133 +10,287 @@ NULL = None -class TestPyFileCAPI(unittest.TestCase): +class _TempFileMixin: def tearDown(self): - try: - os.unlink(os_helper.TESTFN) - except FileNotFoundError: - pass + os_helper.unlink(os_helper.TESTFN) + + +class TestPyFile_FromFd(_TempFileMixin, unittest.TestCase): + # We don't apply heavy testing here, because inside it directly calls + # `_io.open` which is fully tested in `test_io`. def test_file_from_fd(self): - # PyFile_FromFd() - with open(os_helper.TESTFN, "w") as f: - # raise ValueError(*file_mode, 0, "utf-8", "strict", "\n", 0,) + with open(os_helper.TESTFN, "w", encoding="utf-8") as f: file_obj = _testcapi.file_from_fd( f.fileno(), os_helper.TESTFN, "w", 1, "utf-8", "strict", "\n", 0, ) + self.assertIsInstance(file_obj, io.TextIOWrapper) - # We don't apply heavy testing here, because inside it directly calls - # `_io.open` which is fully tested in `test_io`. + def test_name_null(self): + with open(os_helper.TESTFN, "w", encoding="utf-8") as f: + file_obj = _testcapi.file_from_fd( + f.fileno(), NULL, "w", + 1, "utf-8", "strict", "\n", 0, + ) self.assertIsInstance(file_obj, io.TextIOWrapper) - def test_file_get_line(self): - # PyFile_GetLine - get_line = _testcapi.file_get_line + def test_name_invalid_utf(self): + with open(os_helper.TESTFN, "w", encoding="utf-8") as f: + file_obj = _testcapi.file_from_fd( + f.fileno(), "abc\xe9", "w", + 1, "utf-8", "strict", "\n", 0, + ) + self.assertIsInstance(file_obj, io.TextIOWrapper) + + def test_mode_as_null(self): + with open(os_helper.TESTFN, "w", encoding="utf-8") as f: + self.assertRaisesRegex( + TypeError, + r"open\(\) argument 'mode' must be str, not None", + _testcapi.file_from_fd, + f.fileno(), "abc\xe9", NULL, + 1, "utf-8", "strict", "\n", 0, + ) + + def test_string_args_as_null(self): + for arg_pos in (4, 5, 6): + with self.subTest(arg_pos=arg_pos): + with open(os_helper.TESTFN, "w", encoding="utf-8") as f: + args = [ + f.fileno(), os_helper.TESTFN, "w", + 1, "utf-8", "strict", "\n", 0, + ] + args[arg_pos] = NULL + file_obj = _testcapi.file_from_fd(*args) + self.assertIsInstance(file_obj, io.TextIOWrapper) + + def test_string_args_as_invalid_utf(self): + for arg_pos in (4, 5, 6): + with self.subTest(arg_pos=arg_pos): + with open(os_helper.TESTFN, "w", encoding="utf-8") as f: + args = [ + f.fileno(), os_helper.TESTFN, "w", + 1, "utf-8", "strict", "\n", 0, + ] + args[arg_pos] = "\xc3\x28" # invalid utf string + self.assertRaises( + (ValueError, LookupError), + _testcapi.file_from_fd, + *args, + ) + + +class TestPyFile_GetLine(_TempFileMixin, unittest.TestCase): + get_line = _testcapi.file_get_line + + def assertGetLineNegativeIndex(self, first_line, index, eof): + assert index < 0 + if first_line.endswith("\n"): + first_line = first_line.rstrip("\n") + + with open(os_helper.TESTFN, encoding="utf-8") as f: + if eof: + self.assertRaises(EOFError, self.get_line, f, index) + else: + self.assertEqual(self.get_line(f, index), first_line) + + def assertGetLine(self, first_line, eof=False): + with open(os_helper.TESTFN, encoding="utf-8") as f: + self.assertEqual(self.get_line(f, 0), first_line) + with open(os_helper.TESTFN, encoding="utf-8") as f: + self.assertEqual(self.get_line(f, 4), first_line[:4]) + with open(os_helper.TESTFN, encoding="utf-8") as f: + self.assertEqual(self.get_line(f, len(first_line) + 1), first_line) + with open(os_helper.TESTFN, encoding="utf-8") as f: + self.assertEqual(self.get_line(f, _testcapi.INT_MAX), first_line) + self.assertGetLineNegativeIndex(first_line, -1, eof=eof) + self.assertGetLineNegativeIndex(first_line, _testcapi.INT_MIN, eof=eof) + + def test_file_empty_line(self): + first_line = "" + with open(os_helper.TESTFN, "w", encoding="utf-8") as f: + f.writelines([first_line]) + self.assertGetLine(first_line, eof=True) + + def test_file_single_unicode_line(self): + for line_end in ("", "\n"): + with self.subTest(line_end=line_end): + first_line = "text with юникод 统一码" + line_end + with open(os_helper.TESTFN, "w", encoding="utf-8") as f: + f.writelines([first_line]) + self.assertGetLine(first_line) + + def test_file_single_unicode_line_invalid_utf(self): + first_line = "\xc3\x28\n" + with open(os_helper.TESTFN, "w", encoding="utf-8") as f: + f.writelines([first_line]) + self.assertGetLine(first_line) - # Create file with unicode content: - first_line = "text with юникод 统一码" - with open(os_helper.TESTFN, "w") as f: + def test_file_single_unicode_line_encoding_mismatch(self): + first_line = "text with юникод 统一码\n" + with open(os_helper.TESTFN, "w", encoding="utf-8") as f: f.writelines([first_line]) + with open(os_helper.TESTFN, encoding="ascii") as f: + self.assertRaises(UnicodeDecodeError, self.get_line, f, 0) - with open(os_helper.TESTFN) as f: - self.assertEqual(get_line(f, 0), first_line) - with open(os_helper.TESTFN) as f: - self.assertEqual(get_line(f, 4), 'text') - with open(os_helper.TESTFN) as f: - self.assertEqual(get_line(f, -1), first_line) + def test_file_multiple_unicode_lines(self): + first_line = "text with юникод 统一码\n" + with open(os_helper.TESTFN, "w", encoding="utf-8") as f: + f.write(first_line) + f.write("\n".join(["other", "line", ""])) + self.assertGetLine(first_line) - # Create file with bytes content: - first_line = "text with юникод 统一码".encode("utf8") + def test_file_get_multiple_lines(self): + first_line = "text with юникод 统一码\n" + second_line = "second line\n" + with open(os_helper.TESTFN, "w", encoding="utf-8") as f: + f.writelines([first_line, second_line]) + with open(os_helper.TESTFN, encoding="utf-8") as f: + self.assertEqual(self.get_line(f, 0), first_line) + self.assertEqual(self.get_line(f, 0), second_line) + + def test_file_get_line_from_file_like(self): + first_line = "text with юникод 统一码\n" + second_line = "second line\n" + contents = io.StringIO() + contents.writelines([first_line, second_line]) + contents.seek(0) + self.assertEqual(self.get_line(contents, 0), first_line) + self.assertEqual(self.get_line(contents, 0), second_line) + + def test_file_single_bytes_line(self): + # Write bytes directly: + first_line = "text ॐ".encode("utf8") with open(os_helper.TESTFN, mode="wb") as f: f.writelines([first_line]) - with open(os_helper.TESTFN, 'rb') as f: - self.assertEqual(get_line(f, 0), first_line) - with open(os_helper.TESTFN, 'rb') as f: - self.assertEqual(get_line(f, 4), b'text') - with open(os_helper.TESTFN, 'rb') as f: - self.assertEqual(get_line(f, -1), first_line) + with open(os_helper.TESTFN, "rb") as f: + self.assertEqual(self.get_line(f, 0), first_line) + with open(os_helper.TESTFN, "rb") as f: + self.assertEqual(self.get_line(f, 4), b"text") + with open(os_helper.TESTFN, "rb") as f: + self.assertEqual(self.get_line(f, -1), first_line) - # Create empty file: - with open(os_helper.TESTFN, mode="w") as f: + def test_empty_file(self): + with open(os_helper.TESTFN, mode="w", encoding="utf-8"): pass + self.assertGetLine("", eof=True) - with open(os_helper.TESTFN) as f: - self.assertEqual(get_line(f, 0), '') - with open(os_helper.TESTFN) as f: - self.assertEqual(get_line(f, 4), '') - with open(os_helper.TESTFN) as f: - self.assertRaises(EOFError, get_line, f, -1) - - # Not `bytes` or `str` is returned: + def test_wrong_args(self): class _BadIO(io.IOBase): def readline(self, size = 0): return object() - self.assertRaises(TypeError, get_line, _BadIO(), 0) - self.assertRaises(AttributeError, get_line, object(), 0) - # CRASHES: get_line(NULL) + self.assertRaises(TypeError, self.get_line, _BadIO(), 0) + self.assertRaises(AttributeError, self.get_line, object(), 0) + # CRASHES: self.get_line(NULL) - def test_file_write_object(self): - # PyFile_WriteObject - write = _testcapi.file_write_object - def write_and_return(obj, flags=0): - dst = io.StringIO() - write(obj, dst, flags) - return dst.getvalue() +class TestPyFile_WriteObject(_TempFileMixin, unittest.TestCase): + write = _testcapi.file_write_object - self.assertEqual(write_and_return(1), '1') - self.assertEqual(write_and_return('1'), "'1'") - self.assertEqual(write_and_return(False), 'False') - self.assertEqual(write_and_return(b'123'), "b'123'") + def write_and_return(self, obj, flags=0): + dst = io.StringIO() + self.assertEqual(self.write(obj, dst, flags), 0) + return dst.getvalue() + def test_file_write_object(self): + self.assertEqual(self.write_and_return(1), "1") + self.assertEqual( + self.write_and_return("text with юникод 统一码"), + "'text with юникод 统一码'", + ) + self.assertEqual( + self.write_and_return("text with юникод 统一码", flags=_testcapi.Py_PRINT_RAW), + "text with юникод 统一码", + ) + self.assertEqual(self.write_and_return(False), "False") + + def test_file_write_custom_obj(self): class Custom: def __str__(self): - return '' + return "" def __repr__(self): - return '' + return "" - self.assertEqual(write_and_return(Custom()), '') + self.assertEqual(self.write_and_return(Custom()), "") self.assertEqual( - write_and_return(Custom(), flags=_testcapi.Py_PRINT_RAW), - '', + self.write_and_return(Custom(), flags=_testcapi.Py_PRINT_RAW), + "", ) - self.assertRaises(TypeError, write, object(), None) - # CRASHES: write(NULL, io.StringIO(), flags) - # CRASHES: write(NULL, NULL, flags) + def test_file_write_null(self): + self.assertEqual(self.write_and_return(NULL), "") - def test_file_write_string(self): - # PyFile_WriteString - write = _testcapi.file_write_string + def test_file_write_to_real_file(self): + obj = "text with юникод 统一码" + with open(os_helper.TESTFN, mode="w", encoding="utf-8") as f: + self.write(obj, f, _testcapi.Py_PRINT_RAW) + with open(os_helper.TESTFN, encoding="utf-8") as f: + self.assertEqual(f.read(), obj) + + def test_file_write_to_ascii_file(self): + obj = "text with юникод 统一码" + with open(os_helper.TESTFN, mode="w", encoding="ascii") as f: + self.assertRaises( + UnicodeEncodeError, + self.write, obj, f, 0, + ) + self.assertRaises( + UnicodeEncodeError, + self.write, obj, f, _testcapi.Py_PRINT_RAW, + ) + + def test_file_write_invalid(self): + self.assertRaises(TypeError, self.write, object(), io.BytesIO(), 0) + self.assertRaises(AttributeError, self.write, object(), object(), 0) + self.assertRaises(TypeError, self.write, object(), NULL, 0) + self.assertRaises(AttributeError, self.write, NULL, object(), 0) + self.assertRaises(TypeError, self.write, NULL, NULL, 0) - def write_and_return(string): - dst = io.StringIO() - write(string, dst) - return dst.getvalue() - self.assertEqual(write_and_return("abc"), "abc") +class TestPyFile_WriteString(unittest.TestCase): + write = _testcapi.file_write_string + + def write_and_return(self, string): + dst = io.StringIO() + self.assertEqual(self.write(string, dst), 0) + return dst.getvalue() + + def test_file_write_string(self): + self.assertEqual(self.write_and_return("abc"), "abc") self.assertEqual( - write_and_return("text with юникод 统一码"), + self.write_and_return("text with юникод 统一码"), "text with юникод 统一码", ) - self.assertRaises(TypeError, write, object(), io.StringIO()) - self.assertRaises(AttributeError, write, 'abc', object()) - # CRASHES: write('', NULL) + self.assertEqual(self.write_and_return("\xc3\x28"), "\xc3\x28") + + def test_invalid_write(self): + self.assertRaises(AttributeError, self.write, "abc", object()) + self.assertRaises(SystemError, self.write, "", NULL) + self.assertRaises(SystemError, self.write, NULL, NULL) + # CRASHES: self.write(NULL, object()) + + +class TestPyObject_AsFileDescriptor(_TempFileMixin, unittest.TestCase): + as_fd = _testcapi.object_as_file_descriptor def test_object_as_file_descriptor(self): - # PyObject_AsFileDescriptor() - as_fd = _testcapi.object_as_file_descriptor with open(os_helper.TESTFN, mode="w") as f: - self.assertEqual(as_fd(f), f.fileno()) - with open(os_helper.TESTFN, mode="w") as f: - self.assertEqual(as_fd(f.fileno()), f.fileno()) + self.assertEqual(self.as_fd(f), f.fileno()) + self.assertEqual(self.as_fd(f.fileno()), f.fileno()) + def test_incorrect_descriptors(self): class _BadIO(io.IOBase): def fileno(self): return object() # not int - self.assertRaises(TypeError, as_fd, _BadIO()) - self.assertRaises(TypeError, as_fd, object()) - # CRASHES as_fd(NULL) + self.assertRaises(TypeError, self.as_fd, _BadIO()) + self.assertRaises(ValueError, self.as_fd, -1) + self.assertRaises(TypeError, self.as_fd, 1.5) + self.assertRaises(TypeError, self.as_fd, object()) + # CRASHES self.as_fd(NULL) + + +if __name__ == "__main__": + unittest.main() diff --git a/Modules/_testcapi/file.c b/Modules/_testcapi/file.c index 87cbd1128ac4e9..6cc7b7885bf921 100644 --- a/Modules/_testcapi/file.c +++ b/Modules/_testcapi/file.c @@ -2,7 +2,7 @@ #include "util.h" static PyObject * -file_from_fd(PyObject *self, PyObject *args) +file_from_fd(PyObject *Py_UNUSED(self), PyObject *args) { int fd; const char *name; @@ -12,7 +12,7 @@ file_from_fd(PyObject *self, PyObject *args) const char *errors; const char *newline; int closefd; - if (!PyArg_ParseTuple(args, "ississsi", + if (!PyArg_ParseTuple(args, "izzizzzi", &fd, &name, &mode, &buffering, @@ -29,7 +29,7 @@ file_from_fd(PyObject *self, PyObject *args) } static PyObject * -file_get_line(PyObject *self, PyObject *args) +file_get_line(PyObject *Py_UNUSED(self), PyObject *args) { PyObject *obj; int n; @@ -42,7 +42,7 @@ file_get_line(PyObject *self, PyObject *args) } static PyObject * -file_write_object(PyObject *self, PyObject *args) +file_write_object(PyObject *Py_UNUSED(self), PyObject *args) { PyObject *obj, *p; int flags; @@ -56,11 +56,11 @@ file_write_object(PyObject *self, PyObject *args) } static PyObject * -file_write_string(PyObject *self, PyObject *args) +file_write_string(PyObject *Py_UNUSED(self), PyObject *args) { const char *string; PyObject *f; - if (!PyArg_ParseTuple(args, "sO", &string, &f)) { + if (!PyArg_ParseTuple(args, "zO", &string, &f)) { return NULL; } @@ -69,7 +69,7 @@ file_write_string(PyObject *self, PyObject *args) } static PyObject * -object_as_file_descriptor(PyObject *self, PyObject *obj) +object_as_file_descriptor(PyObject *Py_UNUSED(self), PyObject *obj) { NULLABLE(obj); RETURN_INT(PyObject_AsFileDescriptor(obj)); From 3781efb529deb69e8b1d5de7557ea04f63223f56 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sat, 7 Sep 2024 10:13:12 +0300 Subject: [PATCH 3/3] Partially address review, stil need to change invalid utf8 tests --- Lib/test/test_capi/test_file.py | 149 ++++++++++++++++++++------------ 1 file changed, 93 insertions(+), 56 deletions(-) diff --git a/Lib/test/test_capi/test_file.py b/Lib/test/test_capi/test_file.py index 89d2043e7ca51c..5d1f2d0cb83a0f 100644 --- a/Lib/test/test_capi/test_file.py +++ b/Lib/test/test_capi/test_file.py @@ -1,6 +1,5 @@ -import unittest import io -import os +import unittest from test.support import import_helper, os_helper @@ -20,65 +19,77 @@ class TestPyFile_FromFd(_TempFileMixin, unittest.TestCase): # `_io.open` which is fully tested in `test_io`. def test_file_from_fd(self): + from_fd = _testcapi.file_from_fd with open(os_helper.TESTFN, "w", encoding="utf-8") as f: - file_obj = _testcapi.file_from_fd( - f.fileno(), os_helper.TESTFN, "w", - 1, "utf-8", "strict", "\n", 0, - ) - self.assertIsInstance(file_obj, io.TextIOWrapper) + file_obj = from_fd(f.fileno(), os_helper.TESTFN, "w", + 1, "utf-8", "strict", "\n", 0) + self.assertIsInstance(file_obj, io.TextIOWrapper) + self.assertEqual(file_obj.name, f.fileno()) def test_name_null(self): + from_fd = _testcapi.file_from_fd with open(os_helper.TESTFN, "w", encoding="utf-8") as f: - file_obj = _testcapi.file_from_fd( - f.fileno(), NULL, "w", - 1, "utf-8", "strict", "\n", 0, - ) - self.assertIsInstance(file_obj, io.TextIOWrapper) + file_obj = from_fd(f.fileno(), NULL, "w", + 1, "utf-8", "strict", "\n", 0) + self.assertIsInstance(file_obj, io.TextIOWrapper) + self.assertEqual(file_obj.name, f.fileno()) - def test_name_invalid_utf(self): + def test_name_invalid_utf(self): # TODO: use bytes + from_fd = _testcapi.file_from_fd with open(os_helper.TESTFN, "w", encoding="utf-8") as f: - file_obj = _testcapi.file_from_fd( - f.fileno(), "abc\xe9", "w", - 1, "utf-8", "strict", "\n", 0, - ) + file_obj = from_fd(f.fileno(), "abc\xe9", "w", + 1, "utf-8", "strict", "\n", 0) self.assertIsInstance(file_obj, io.TextIOWrapper) def test_mode_as_null(self): + from_fd = _testcapi.file_from_fd with open(os_helper.TESTFN, "w", encoding="utf-8") as f: - self.assertRaisesRegex( + with self.assertRaisesRegex( TypeError, r"open\(\) argument 'mode' must be str, not None", - _testcapi.file_from_fd, - f.fileno(), "abc\xe9", NULL, - 1, "utf-8", "strict", "\n", 0, - ) + ): + from_fd(f.fileno(), "abc\xe9", NULL, + 1, "utf-8", "strict", "\n", 0) def test_string_args_as_null(self): - for arg_pos in (4, 5, 6): - with self.subTest(arg_pos=arg_pos): - with open(os_helper.TESTFN, "w", encoding="utf-8") as f: - args = [ - f.fileno(), os_helper.TESTFN, "w", - 1, "utf-8", "strict", "\n", 0, - ] - args[arg_pos] = NULL - file_obj = _testcapi.file_from_fd(*args) - self.assertIsInstance(file_obj, io.TextIOWrapper) + from_fd = _testcapi.file_from_fd + + with open(os_helper.TESTFN, "w", encoding="utf-8") as f: + file_obj = from_fd(f.fileno(), os_helper.TESTFN, "w", + 1, NULL, "strict", "\n", 0) + self.assertIsInstance(file_obj, io.TextIOWrapper) + self.assertEqual(file_obj.encoding, "UTF-8") + + with open(os_helper.TESTFN, "w", encoding="utf-8") as f: + file_obj = from_fd(f.fileno(), os_helper.TESTFN, "w", + 1, "utf-8", NULL, "\n", 0) + self.assertIsInstance(file_obj, io.TextIOWrapper) + self.assertEqual(file_obj.errors, "strict") + + with open(os_helper.TESTFN, "w", encoding="utf-8") as f: + file_obj = from_fd(f.fileno(), os_helper.TESTFN, "w", + 1, "utf-8", "strict", NULL, 0) + self.assertIsInstance(file_obj, io.TextIOWrapper) + self.assertIsNone(file_obj.newlines) def test_string_args_as_invalid_utf(self): - for arg_pos in (4, 5, 6): - with self.subTest(arg_pos=arg_pos): - with open(os_helper.TESTFN, "w", encoding="utf-8") as f: - args = [ - f.fileno(), os_helper.TESTFN, "w", - 1, "utf-8", "strict", "\n", 0, - ] - args[arg_pos] = "\xc3\x28" # invalid utf string - self.assertRaises( - (ValueError, LookupError), - _testcapi.file_from_fd, - *args, - ) + from_fd = _testcapi.file_from_fd + invalid_utf = "\xc3\x28" # TODO: use bytes + + with open(os_helper.TESTFN, "w", encoding="utf-8") as f: + with self.assertRaises((ValueError, LookupError)): + from_fd(f.fileno(), os_helper.TESTFN, "w", + 1, invalid_utf, "strict", "\n", 0) + + with open(os_helper.TESTFN, "w", encoding="utf-8") as f: + with self.assertRaises((ValueError, LookupError)): + from_fd(f.fileno(), os_helper.TESTFN, "w", + 1, "utf-8", invalid_utf, "\n", 0) + + with open(os_helper.TESTFN, "w", encoding="utf-8") as f: + with self.assertRaises((ValueError, LookupError)): + from_fd(f.fileno(), os_helper.TESTFN, "w", + 1, "utf-8", "strict", invalid_utf, 0) class TestPyFile_GetLine(_TempFileMixin, unittest.TestCase): @@ -109,8 +120,8 @@ def assertGetLine(self, first_line, eof=False): def test_file_empty_line(self): first_line = "" - with open(os_helper.TESTFN, "w", encoding="utf-8") as f: - f.writelines([first_line]) + with open(os_helper.TESTFN, "w", encoding="utf-8"): + pass self.assertGetLine(first_line, eof=True) def test_file_single_unicode_line(self): @@ -122,7 +133,7 @@ def test_file_single_unicode_line(self): self.assertGetLine(first_line) def test_file_single_unicode_line_invalid_utf(self): - first_line = "\xc3\x28\n" + first_line = "\xc3\x28\n" # TODO: use bytes with open(os_helper.TESTFN, "w", encoding="utf-8") as f: f.writelines([first_line]) self.assertGetLine(first_line) @@ -153,9 +164,7 @@ def test_file_get_multiple_lines(self): def test_file_get_line_from_file_like(self): first_line = "text with юникод 统一码\n" second_line = "second line\n" - contents = io.StringIO() - contents.writelines([first_line, second_line]) - contents.seek(0) + contents = io.StringIO(f"{first_line}{second_line}") self.assertEqual(self.get_line(contents, 0), first_line) self.assertEqual(self.get_line(contents, 0), second_line) @@ -220,6 +229,23 @@ def __repr__(self): "", ) + def test_file_write_custom_obj_raises(self): + class ReprRaises: + def __repr__(self): + raise ValueError("repr raised") + + with self.assertRaisesRegex(ValueError, "repr raised"): + self.write_and_return(ReprRaises()) + with self.assertRaisesRegex(ValueError, "repr raised"): + self.write_and_return(ReprRaises(), flags=_testcapi.Py_PRINT_RAW) + + class StrRaises: + def __str__(self): + raise ValueError("str raised") + + with self.assertRaisesRegex(ValueError, "str raised"): + self.write_and_return(StrRaises(), flags=_testcapi.Py_PRINT_RAW) + def test_file_write_null(self): self.assertEqual(self.write_and_return(NULL), "") @@ -243,11 +269,21 @@ def test_file_write_to_ascii_file(self): ) def test_file_write_invalid(self): - self.assertRaises(TypeError, self.write, object(), io.BytesIO(), 0) - self.assertRaises(AttributeError, self.write, object(), object(), 0) - self.assertRaises(TypeError, self.write, object(), NULL, 0) - self.assertRaises(AttributeError, self.write, NULL, object(), 0) - self.assertRaises(TypeError, self.write, NULL, NULL, 0) + wr = self.write + self.assertRaises(TypeError, wr, object(), io.BytesIO(), 0) + self.assertRaises(AttributeError, wr, object(), object(), 0) + self.assertRaises(TypeError, wr, object(), NULL, 0) + self.assertRaises(AttributeError, wr, NULL, object(), 0) + self.assertRaises(TypeError, wr, NULL, NULL, 0) + + def test_file_write_invalid_print_raw(self): + wr = self.write + raw = _testcapi.Py_PRINT_RAW + self.assertRaises(TypeError, wr, object(), io.BytesIO(), raw) + self.assertRaises(AttributeError, wr, object(), object(), raw) + self.assertRaises(TypeError, wr, object(), NULL, raw) + self.assertRaises(AttributeError, wr, NULL, object(), raw) + self.assertRaises(TypeError, wr, NULL, NULL, raw) class TestPyFile_WriteString(unittest.TestCase): @@ -264,6 +300,7 @@ def test_file_write_string(self): self.write_and_return("text with юникод 统一码"), "text with юникод 统一码", ) + # TODO: use real invalid utf8 via bytes self.assertEqual(self.write_and_return("\xc3\x28"), "\xc3\x28") def test_invalid_write(self):