diff --git a/Doc/c-api/unicode.rst b/Doc/c-api/unicode.rst index 5383e9787f2..019453fac89 100644 --- a/Doc/c-api/unicode.rst +++ b/Doc/c-api/unicode.rst @@ -810,13 +810,16 @@ used, passing :c:func:`PyUnicode_FSConverter` as the conversion function: .. c:function:: int PyUnicode_FSConverter(PyObject* obj, void* result) - ParseTuple converter: encode :class:`str` objects to :class:`bytes` using + ParseTuple converter: encode :class:`str` objects -- obtained directly or + through the :class:`os.PathLike` interface -- to :class:`bytes` using :c:func:`PyUnicode_EncodeFSDefault`; :class:`bytes` objects are output as-is. *result* must be a :c:type:`PyBytesObject*` which must be released when it is no longer used. .. versionadded:: 3.1 + .. versionchanged:: 3.6 + Accepts a :term:`path-like object`. To decode file names during argument parsing, the ``"O&"`` converter should be used, passing :c:func:`PyUnicode_FSDecoder` as the conversion function: diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py index c5bea851834..d79a32fc710 100644 --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -100,6 +100,21 @@ def bytes_filename_warn(expected): yield +class _PathLike(os.PathLike): + + def __init__(self, path=""): + self.path = path + + def __str__(self): + return str(self.path) + + def __fspath__(self): + if isinstance(self.path, BaseException): + raise self.path + else: + return self.path + + def create_file(filename, content=b'content'): with open(filename, "xb", 0) as fp: fp.write(content) @@ -894,15 +909,7 @@ class WalkTests(unittest.TestCase): self.assertEqual(all[1], self.sub2_tree) def test_file_like_path(self): - class FileLike: - def __init__(self, path): - self._path = path - def __str__(self): - return str(self._path) - def __fspath__(self): - return self._path - - self.test_walk_prune(FileLike(self.walk_path)) + self.test_walk_prune(_PathLike(self.walk_path)) def test_walk_bottom_up(self): # Walk bottom-up. @@ -2124,7 +2131,8 @@ class PidTests(unittest.TestCase): def test_waitpid(self): args = [sys.executable, '-c', 'pass'] - pid = os.spawnv(os.P_NOWAIT, args[0], args) + # Add an implicit test for PyUnicode_FSConverter(). + pid = os.spawnv(os.P_NOWAIT, _PathLike(args[0]), args) status = os.waitpid(pid, 0) self.assertEqual(status, (pid, 0)) @@ -2833,25 +2841,18 @@ class PathTConverterTests(unittest.TestCase): ] def test_path_t_converter(self): - class PathLike: - def __init__(self, path): - self.path = path - - def __fspath__(self): - return self.path - str_filename = support.TESTFN if os.name == 'nt': bytes_fspath = bytes_filename = None else: bytes_filename = support.TESTFN.encode('ascii') - bytes_fspath = PathLike(bytes_filename) - fd = os.open(PathLike(str_filename), os.O_WRONLY|os.O_CREAT) + bytes_fspath = _PathLike(bytes_filename) + fd = os.open(_PathLike(str_filename), os.O_WRONLY|os.O_CREAT) self.addCleanup(support.unlink, support.TESTFN) self.addCleanup(os.close, fd) - int_fspath = PathLike(fd) - str_fspath = PathLike(str_filename) + int_fspath = _PathLike(fd) + str_fspath = _PathLike(str_filename) for name, allow_fd, extra_args, cleanup_fn in self.functions: with self.subTest(name=name): @@ -3205,15 +3206,6 @@ class TestPEP519(unittest.TestCase): # if a C version is provided. fspath = staticmethod(os.fspath) - class PathLike: - def __init__(self, path=''): - self.path = path - def __fspath__(self): - if isinstance(self.path, BaseException): - raise self.path - else: - return self.path - def test_return_bytes(self): for b in b'hello', b'goodbye', b'some/path/and/file': self.assertEqual(b, self.fspath(b)) @@ -3224,16 +3216,16 @@ class TestPEP519(unittest.TestCase): def test_fsencode_fsdecode(self): for p in "path/like/object", b"path/like/object": - pathlike = self.PathLike(p) + pathlike = _PathLike(p) self.assertEqual(p, self.fspath(pathlike)) self.assertEqual(b"path/like/object", os.fsencode(pathlike)) self.assertEqual("path/like/object", os.fsdecode(pathlike)) def test_pathlike(self): - self.assertEqual('#feelthegil', self.fspath(self.PathLike('#feelthegil'))) - self.assertTrue(issubclass(self.PathLike, os.PathLike)) - self.assertTrue(isinstance(self.PathLike(), os.PathLike)) + self.assertEqual('#feelthegil', self.fspath(_PathLike('#feelthegil'))) + self.assertTrue(issubclass(_PathLike, os.PathLike)) + self.assertTrue(isinstance(_PathLike(), os.PathLike)) def test_garbage_in_exception_out(self): vapor = type('blah', (), {}) @@ -3245,14 +3237,14 @@ class TestPEP519(unittest.TestCase): def test_bad_pathlike(self): # __fspath__ returns a value other than str or bytes. - self.assertRaises(TypeError, self.fspath, self.PathLike(42)) + self.assertRaises(TypeError, self.fspath, _PathLike(42)) # __fspath__ attribute that is not callable. c = type('foo', (), {}) c.__fspath__ = 1 self.assertRaises(TypeError, self.fspath, c()) # __fspath__ raises an exception. self.assertRaises(ZeroDivisionError, self.fspath, - self.PathLike(ZeroDivisionError())) + _PathLike(ZeroDivisionError())) # Only test if the C version is provided, otherwise TestPEP519 already tested # the pure Python implementation. diff --git a/Misc/NEWS b/Misc/NEWS index 88fcd9869d6..34aa42e2a4f 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -188,6 +188,11 @@ Library - Issue #27573: exit message for code.interact is now configurable. +C API +----- + +- Issue #26027: Add support for path-like objects in PyUnicode_FSConverter(). + Tests ----- diff --git a/Modules/clinic/posixmodule.c.h b/Modules/clinic/posixmodule.c.h index 4b44257cfbc..b99f1806f3c 100644 --- a/Modules/clinic/posixmodule.c.h +++ b/Modules/clinic/posixmodule.c.h @@ -3011,13 +3011,13 @@ PyDoc_STRVAR(os_waitpid__doc__, {"waitpid", (PyCFunction)os_waitpid, METH_VARARGS, os_waitpid__doc__}, static PyObject * -os_waitpid_impl(PyObject *module, Py_intptr_t pid, int options); +os_waitpid_impl(PyObject *module, intptr_t pid, int options); static PyObject * os_waitpid(PyObject *module, PyObject *args) { PyObject *return_value = NULL; - Py_intptr_t pid; + intptr_t pid; int options; if (!PyArg_ParseTuple(args, "" _Py_PARSE_INTPTR "i:waitpid", @@ -5479,13 +5479,13 @@ PyDoc_STRVAR(os_get_handle_inheritable__doc__, {"get_handle_inheritable", (PyCFunction)os_get_handle_inheritable, METH_O, os_get_handle_inheritable__doc__}, static int -os_get_handle_inheritable_impl(PyObject *module, Py_intptr_t handle); +os_get_handle_inheritable_impl(PyObject *module, intptr_t handle); static PyObject * os_get_handle_inheritable(PyObject *module, PyObject *arg) { PyObject *return_value = NULL; - Py_intptr_t handle; + intptr_t handle; int _return_value; if (!PyArg_Parse(arg, "" _Py_PARSE_INTPTR ":get_handle_inheritable", &handle)) { @@ -5515,14 +5515,14 @@ PyDoc_STRVAR(os_set_handle_inheritable__doc__, {"set_handle_inheritable", (PyCFunction)os_set_handle_inheritable, METH_VARARGS, os_set_handle_inheritable__doc__}, static PyObject * -os_set_handle_inheritable_impl(PyObject *module, Py_intptr_t handle, +os_set_handle_inheritable_impl(PyObject *module, intptr_t handle, int inheritable); static PyObject * os_set_handle_inheritable(PyObject *module, PyObject *args) { PyObject *return_value = NULL; - Py_intptr_t handle; + intptr_t handle; int inheritable; if (!PyArg_ParseTuple(args, "" _Py_PARSE_INTPTR "p:set_handle_inheritable", @@ -6042,4 +6042,4 @@ exit: #ifndef OS_SET_HANDLE_INHERITABLE_METHODDEF #define OS_SET_HANDLE_INHERITABLE_METHODDEF #endif /* !defined(OS_SET_HANDLE_INHERITABLE_METHODDEF) */ -/*[clinic end generated code: output=2b85bb3703a6488a input=a9049054013a1b77]*/ +/*[clinic end generated code: output=677ce794fb126161 input=a9049054013a1b77]*/ diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index e88ee56fa01..beea9e08121 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -2520,7 +2520,7 @@ class sched_param_converter(CConverter): impl_by_reference = True; [python start generated code]*/ -/*[python end generated code: output=da39a3ee5e6b4b0d input=affe68316f160401]*/ +/*[python end generated code: output=da39a3ee5e6b4b0d input=418fce0e01144461]*/ /*[clinic input] @@ -7092,7 +7092,7 @@ The options argument is ignored on Windows. static PyObject * os_waitpid_impl(PyObject *module, intptr_t pid, int options) -/*[clinic end generated code: output=15f1ce005a346b09 input=444c8f51cca5b862]*/ +/*[clinic end generated code: output=be836b221271d538 input=40f2440c515410f8]*/ { int status; intptr_t res; @@ -11383,7 +11383,7 @@ Get the close-on-exe flag of the specified file descriptor. static int os_get_handle_inheritable_impl(PyObject *module, intptr_t handle) -/*[clinic end generated code: output=9e5389b0aa0916ce input=5f7759443aae3dc5]*/ +/*[clinic end generated code: output=36be5afca6ea84d8 input=cfe99f9c05c70ad1]*/ { DWORD flags; @@ -11408,7 +11408,7 @@ Set the inheritable flag of the specified handle. static PyObject * os_set_handle_inheritable_impl(PyObject *module, intptr_t handle, int inheritable) -/*[clinic end generated code: output=b1e67bfa3213d745 input=e64b2b2730469def]*/ +/*[clinic end generated code: output=021d74fe6c96baa3 input=7a7641390d8364fc]*/ { DWORD flags = inheritable ? HANDLE_FLAG_INHERIT : 0; if (!SetHandleInformation((HANDLE)handle, HANDLE_FLAG_INHERIT, flags)) { diff --git a/Objects/unicodeobject.c b/Objects/unicodeobject.c index 86f23c9c7ce..b96333ce472 100644 --- a/Objects/unicodeobject.c +++ b/Objects/unicodeobject.c @@ -3842,6 +3842,7 @@ PyUnicode_DecodeFSDefaultAndSize(const char *s, Py_ssize_t size) int PyUnicode_FSConverter(PyObject* arg, void* addr) { + PyObject *path = NULL; PyObject *output = NULL; Py_ssize_t size; void *data; @@ -3850,22 +3851,22 @@ PyUnicode_FSConverter(PyObject* arg, void* addr) *(PyObject**)addr = NULL; return 1; } - if (PyBytes_Check(arg)) { - output = arg; - Py_INCREF(output); - } - else if (PyUnicode_Check(arg)) { - output = PyUnicode_EncodeFSDefault(arg); - if (!output) - return 0; - assert(PyBytes_Check(output)); - } - else { - PyErr_Format(PyExc_TypeError, - "must be str or bytes, not %.100s", - Py_TYPE(arg)->tp_name); + path = PyOS_FSPath(arg); + if (path == NULL) { return 0; } + if (PyBytes_Check(path)) { + output = path; + } + else { // PyOS_FSPath() guarantees its returned value is bytes or str. + output = PyUnicode_EncodeFSDefault(path); + Py_DECREF(path); + if (!output) { + return 0; + } + assert(PyBytes_Check(output)); + } + size = PyBytes_GET_SIZE(output); data = PyBytes_AS_STRING(output); if ((size_t)size != strlen(data)) {