diff -r f10029fea6ee Lib/test/test_os.py --- a/Lib/test/test_os.py Fri Aug 05 12:57:38 2016 -0700 +++ b/Lib/test/test_os.py Fri Aug 05 14:55:27 2016 -0700 @@ -2800,6 +2800,70 @@ self.assertEqual(os.get_inheritable(slave_fd), False) +class PathTConverterTests(unittest.TestCase): + # tuples of (function name, allows fd arguments, additional arguments to + # function, cleanup function) + functions = [ + ('stat', True, (), None), + ('lstat', False, (), None), + ('access', True, (os.F_OK,), None), + ('chflags', False, (0,), None), + ('lchflags', False, (0,), None), + ('open', False, (0,), getattr(os, 'close', None)), + ] + + 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 + bytes_filename = support.TESTFN.encode('ascii') + bytearray_filename = bytearray(bytes_filename) + fd = os.open(PathLike(str_filename), os.O_WRONLY|os.O_CREAT) + self.addCleanup(os.close, fd) + self.addCleanup(support.unlink, support.TESTFN) + + int_fspath = PathLike(fd) + str_fspath = PathLike(str_filename) + bytes_fspath = PathLike(bytes_filename) + bytearray_fspath = PathLike(bytearray_filename) + + for name, allow_fd, extra_args, cleanup_fn in self.functions: + with self.subTest(name=name): + try: + fn = getattr(os, name) + except AttributeError: + continue + + for path in (str_filename, bytes_filename, bytearray_filename, + str_fspath, bytes_fspath): + with self.subTest(name=name, path=path): + result = fn(path, *extra_args) + if cleanup_fn is not None: + cleanup_fn(result) + + with self.assertRaisesRegex( + TypeError, 'should be string, bytes'): + fn(int_fspath, *extra_args) + with self.assertRaisesRegex( + TypeError, 'should be string, bytes'): + fn(bytearray_fspath, *extra_args) + + if allow_fd: + result = fn(fd, *extra_args) # should not fail + if cleanup_fn is not None: + cleanup_fn(result) + else: + with self.assertRaisesRegex( + TypeError, + 'os.PathLike'): + fn(fd, *extra_args) + + @unittest.skipUnless(hasattr(os, 'get_blocking'), 'needs os.get_blocking() and os.set_blocking()') class BlockingTests(unittest.TestCase): diff -r f10029fea6ee Lib/test/test_posix.py --- a/Lib/test/test_posix.py Fri Aug 05 12:57:38 2016 -0700 +++ b/Lib/test/test_posix.py Fri Aug 05 14:55:27 2016 -0700 @@ -397,7 +397,7 @@ self.assertTrue(posix.stat(fp.fileno())) self.assertRaisesRegex(TypeError, - 'should be string, bytes or integer, not', + 'should be string, bytes, os.PathLike or integer, not', posix.stat, float(fp.fileno())) finally: fp.close() @@ -410,13 +410,13 @@ self.assertTrue(posix.stat(bytearray(os.fsencode(support.TESTFN)))) self.assertRaisesRegex(TypeError, - 'should be string, bytes or integer, not', + 'should be string, bytes, os.PathLike or integer, not', posix.stat, None) self.assertRaisesRegex(TypeError, - 'should be string, bytes or integer, not', + 'should be string, bytes, os.PathLike or integer, not', posix.stat, list(support.TESTFN)) self.assertRaisesRegex(TypeError, - 'should be string, bytes or integer, not', + 'should be string, bytes, os.PathLike or integer, not', posix.stat, list(os.fsencode(support.TESTFN))) @unittest.skipUnless(hasattr(posix, 'mkfifo'), "don't have mkfifo()") diff -r f10029fea6ee Modules/posixmodule.c --- a/Modules/posixmodule.c Fri Aug 05 12:57:38 2016 -0700 +++ b/Modules/posixmodule.c Fri Aug 05 14:55:27 2016 -0700 @@ -834,8 +834,9 @@ path_converter(PyObject *o, void *p) { path_t *path = (path_t *)p; - PyObject *bytes; + PyObject *bytes, *to_cleanup = NULL; Py_ssize_t length; + int ret, is_index, is_buffer; const char *narrow; #define FORMAT_EXCEPTION(exc, fmt) \ @@ -862,21 +863,46 @@ return 1; } + /* only call this here so that we don't treat the return value of + os.fspath() as an fd or buffer */ + is_index = path->allow_fd && PyIndex_Check(o); + is_buffer = PyObject_CheckBuffer(o); + + if (!is_index && !is_buffer && !PyUnicode_Check(o) && !PyBytes_Check(o)) { + /* Inline PyOS_FSPath() for better error messages. */ + _Py_IDENTIFIER(__fspath__); + PyObject *func = NULL; + + func = _PyObject_LookupSpecial(o, &PyId___fspath__); + if (NULL == func) { + goto error_exit; + } + + o = to_cleanup = PyObject_CallFunctionObjArgs(func, NULL); + Py_DECREF(func); + if (NULL == o) { + goto error_exit; + } + } + if (PyUnicode_Check(o)) { #ifdef MS_WINDOWS const wchar_t *wide; wide = PyUnicode_AsUnicodeAndSize(o, &length); if (!wide) { - return 0; + ret = 0; + goto exit; } if (length > 32767) { FORMAT_EXCEPTION(PyExc_ValueError, "%s too long for Windows"); - return 0; + ret = 0; + goto exit; } if (wcslen(wide) != length) { FORMAT_EXCEPTION(PyExc_ValueError, "embedded null character in %s"); - return 0; + ret = 0; + goto exit; } path->wide = wide; @@ -884,45 +910,54 @@ path->length = length; path->object = o; path->fd = -1; - return 1; + ret = 1; + goto exit; #else if (!PyUnicode_FSConverter(o, &bytes)) { - return 0; - } -#endif - } - else if (PyObject_CheckBuffer(o)) { + ret = 0; + goto exit; + } +#endif + } + /* either o was passed in as a buffer, or PyOS_FSPath returned a bytes */ + else if (is_buffer || PyBytes_Check(o)) { #ifdef MS_WINDOWS if (win32_warn_bytes_api()) { - return 0; + ret = 0; + goto exit; } #endif bytes = PyBytes_FromObject(o); if (!bytes) { - return 0; - } - } - else if (path->allow_fd && PyIndex_Check(o)) { + ret = 0; + goto exit; + } + } + else if (is_index) { if (!_fd_converter(o, &path->fd)) { - return 0; + ret = 0; + goto exit; } path->wide = NULL; path->narrow = NULL; path->length = 0; path->object = o; - return 1; + ret = 1; + goto exit; } else { +error_exit: PyErr_Format(PyExc_TypeError, "%s%s%s should be %s, not %.200s", path->function_name ? path->function_name : "", path->function_name ? ": " : "", path->argument_name ? path->argument_name : "path", - path->allow_fd && path->nullable ? "string, bytes, integer or None" : - path->allow_fd ? "string, bytes or integer" : - path->nullable ? "string, bytes or None" : - "string or bytes", + path->allow_fd && path->nullable ? "string, bytes, os.PathLike, integer or None" : + path->allow_fd ? "string, bytes, os.PathLike or integer" : + path->nullable ? "string, bytes, os.PathLike or None" : + "string, bytes or os.PathLike", Py_TYPE(o)->tp_name); - return 0; + ret = 0; + goto exit; } length = PyBytes_GET_SIZE(bytes); @@ -930,7 +965,8 @@ if (length > MAX_PATH-1) { FORMAT_EXCEPTION(PyExc_ValueError, "%s too long for Windows"); Py_DECREF(bytes); - return 0; + ret = 0; + goto exit; } #endif @@ -938,7 +974,8 @@ if ((size_t)length != strlen(narrow)) { FORMAT_EXCEPTION(PyExc_ValueError, "embedded null character in %s"); Py_DECREF(bytes); - return 0; + ret = 0; + goto exit; } path->wide = NULL; @@ -947,7 +984,11 @@ path->object = o; path->fd = -1; path->cleanup = bytes; - return Py_CLEANUP_SUPPORTED; + ret = Py_CLEANUP_SUPPORTED; + +exit: + Py_XDECREF(to_cleanup); + return ret; } static void @@ -12302,6 +12343,8 @@ PyObject * PyOS_FSPath(PyObject *path) { + /* For error message reasons, this function is manually inlined in + path_converter(). */ _Py_IDENTIFIER(__fspath__); PyObject *func = NULL; PyObject *path_repr = NULL;