mirror of https://github.com/python/cpython.git
Issue #27186: Update os.fspath()/PyOS_FSPath() to check the return
type of __fspath__(). As part of this change, also make sure that the pure Python implementation of os.fspath() is tested.
This commit is contained in:
parent
19b2a53a82
commit
c78ca1e044
|
@ -10,8 +10,9 @@ Operating System Utilities
|
||||||
Return the file system representation for *path*. If the object is a
|
Return the file system representation for *path*. If the object is a
|
||||||
:class:`str` or :class:`bytes` object, then its reference count is
|
:class:`str` or :class:`bytes` object, then its reference count is
|
||||||
incremented. If the object implements the :class:`os.PathLike` interface,
|
incremented. If the object implements the :class:`os.PathLike` interface,
|
||||||
then ``type(path).__fspath__()`` is returned. Otherwise :exc:`TypeError` is
|
then :meth:`~os.PathLike.__fspath__` is returned as long as it is a
|
||||||
raised and ``NULL`` is returned.
|
:class:`str` or :class:`bytes` object. Otherwise :exc:`TypeError` is raised
|
||||||
|
and ``NULL`` is returned.
|
||||||
|
|
||||||
.. versionadded:: 3.6
|
.. versionadded:: 3.6
|
||||||
|
|
||||||
|
|
|
@ -179,7 +179,8 @@ process and user.
|
||||||
.. versionadded:: 3.2
|
.. versionadded:: 3.2
|
||||||
|
|
||||||
.. versionchanged:: 3.6
|
.. versionchanged:: 3.6
|
||||||
Support added to accept objects implementing :class:`os.PathLike`.
|
Support added to accept objects implementing the :class:`os.PathLike`
|
||||||
|
interface.
|
||||||
|
|
||||||
|
|
||||||
.. function:: fsdecode(filename)
|
.. function:: fsdecode(filename)
|
||||||
|
@ -192,17 +193,18 @@ process and user.
|
||||||
.. versionadded:: 3.2
|
.. versionadded:: 3.2
|
||||||
|
|
||||||
.. versionchanged:: 3.6
|
.. versionchanged:: 3.6
|
||||||
Support added to accept objects implementing :class:`os.PathLike`.
|
Support added to accept objects implementing the :class:`os.PathLike`
|
||||||
|
interface.
|
||||||
|
|
||||||
|
|
||||||
.. function:: fspath(path)
|
.. function:: fspath(path)
|
||||||
|
|
||||||
Return the file system representation of the path.
|
Return the file system representation of the path.
|
||||||
|
|
||||||
If :class:`str` or :class:`bytes` is passed in, it is returned unchanged;
|
If :class:`str` or :class:`bytes` is passed in, it is returned unchanged.
|
||||||
otherwise, the result of calling ``type(path).__fspath__`` is returned
|
Otherwise :meth:`~os.PathLike.__fspath__` is called and its value is
|
||||||
(which is represented by :class:`os.PathLike`). All other types raise a
|
returned as long as it is a :class:`str` or :class:`bytes` object.
|
||||||
:exc:`TypeError`.
|
In all other cases, :exc:`TypeError` is raised.
|
||||||
|
|
||||||
.. versionadded:: 3.6
|
.. versionadded:: 3.6
|
||||||
|
|
||||||
|
|
51
Lib/os.py
51
Lib/os.py
|
@ -881,14 +881,11 @@ def fsencode(filename):
|
||||||
On Windows, use 'strict' error handler if the file system encoding is
|
On Windows, use 'strict' error handler if the file system encoding is
|
||||||
'mbcs' (which is the default encoding).
|
'mbcs' (which is the default encoding).
|
||||||
"""
|
"""
|
||||||
filename = fspath(filename)
|
filename = fspath(filename) # Does type-checking of `filename`.
|
||||||
if isinstance(filename, bytes):
|
if isinstance(filename, str):
|
||||||
return filename
|
|
||||||
elif isinstance(filename, str):
|
|
||||||
return filename.encode(encoding, errors)
|
return filename.encode(encoding, errors)
|
||||||
else:
|
else:
|
||||||
raise TypeError("expected str, bytes or os.PathLike object, not "
|
return filename
|
||||||
+ type(filename).__name__)
|
|
||||||
|
|
||||||
def fsdecode(filename):
|
def fsdecode(filename):
|
||||||
"""Decode filename (an os.PathLike, bytes, or str) from the filesystem
|
"""Decode filename (an os.PathLike, bytes, or str) from the filesystem
|
||||||
|
@ -896,14 +893,11 @@ def fsdecode(filename):
|
||||||
Windows, use 'strict' error handler if the file system encoding is
|
Windows, use 'strict' error handler if the file system encoding is
|
||||||
'mbcs' (which is the default encoding).
|
'mbcs' (which is the default encoding).
|
||||||
"""
|
"""
|
||||||
filename = fspath(filename)
|
filename = fspath(filename) # Does type-checking of `filename`.
|
||||||
if isinstance(filename, str):
|
if isinstance(filename, bytes):
|
||||||
return filename
|
|
||||||
elif isinstance(filename, bytes):
|
|
||||||
return filename.decode(encoding, errors)
|
return filename.decode(encoding, errors)
|
||||||
else:
|
else:
|
||||||
raise TypeError("expected str, bytes or os.PathLike object, not "
|
return filename
|
||||||
+ type(filename).__name__)
|
|
||||||
|
|
||||||
return fsencode, fsdecode
|
return fsencode, fsdecode
|
||||||
|
|
||||||
|
@ -1102,12 +1096,16 @@ def fdopen(fd, *args, **kwargs):
|
||||||
import io
|
import io
|
||||||
return io.open(fd, *args, **kwargs)
|
return io.open(fd, *args, **kwargs)
|
||||||
|
|
||||||
# Supply os.fspath() if not defined in C
|
|
||||||
if not _exists('fspath'):
|
|
||||||
def fspath(path):
|
|
||||||
"""Return the string representation of the path.
|
|
||||||
|
|
||||||
If str or bytes is passed in, it is returned unchanged.
|
# For testing purposes, make sure the function is available when the C
|
||||||
|
# implementation exists.
|
||||||
|
def _fspath(path):
|
||||||
|
"""Return the path representation of a path-like object.
|
||||||
|
|
||||||
|
If str or bytes is passed in, it is returned unchanged. Otherwise the
|
||||||
|
os.PathLike interface is used to get the path representation. If the
|
||||||
|
path representation is not str or bytes, TypeError is raised. If the
|
||||||
|
provided path is not str, bytes, or os.PathLike, TypeError is raised.
|
||||||
"""
|
"""
|
||||||
if isinstance(path, (str, bytes)):
|
if isinstance(path, (str, bytes)):
|
||||||
return path
|
return path
|
||||||
|
@ -1116,13 +1114,26 @@ def fspath(path):
|
||||||
# methods.
|
# methods.
|
||||||
path_type = type(path)
|
path_type = type(path)
|
||||||
try:
|
try:
|
||||||
return path_type.__fspath__(path)
|
path_repr = path_type.__fspath__(path)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
if hasattr(path_type, '__fspath__'):
|
if hasattr(path_type, '__fspath__'):
|
||||||
raise
|
raise
|
||||||
|
else:
|
||||||
|
raise TypeError("expected str, bytes or os.PathLike object, "
|
||||||
|
"not " + path_type.__name__)
|
||||||
|
if isinstance(path_repr, (str, bytes)):
|
||||||
|
return path_repr
|
||||||
|
else:
|
||||||
|
raise TypeError("expected {}.__fspath__() to return str or bytes, "
|
||||||
|
"not {}".format(path_type.__name__,
|
||||||
|
type(path_repr).__name__))
|
||||||
|
|
||||||
|
# If there is no C implementation, make the pure Python version the
|
||||||
|
# implementation as transparently as possible.
|
||||||
|
if not _exists('fspath'):
|
||||||
|
fspath = _fspath
|
||||||
|
fspath.__name__ = "fspath"
|
||||||
|
|
||||||
raise TypeError("expected str, bytes or os.PathLike object, not "
|
|
||||||
+ path_type.__name__)
|
|
||||||
|
|
||||||
class PathLike(abc.ABC):
|
class PathLike(abc.ABC):
|
||||||
|
|
||||||
|
|
|
@ -879,7 +879,7 @@ def check_path_succeeds(path):
|
||||||
check_path_succeeds(PathLike(support.TESTFN.encode('utf-8')))
|
check_path_succeeds(PathLike(support.TESTFN.encode('utf-8')))
|
||||||
|
|
||||||
bad_path = PathLike(TypeError)
|
bad_path = PathLike(TypeError)
|
||||||
with self.assertRaisesRegex(TypeError, 'invalid file'):
|
with self.assertRaises(TypeError):
|
||||||
self.open(bad_path, 'w')
|
self.open(bad_path, 'w')
|
||||||
|
|
||||||
# ensure that refcounting is correct with some error conditions
|
# ensure that refcounting is correct with some error conditions
|
||||||
|
|
|
@ -3112,55 +3112,59 @@ def test_resource_warning(self):
|
||||||
|
|
||||||
|
|
||||||
class TestPEP519(unittest.TestCase):
|
class TestPEP519(unittest.TestCase):
|
||||||
"os.fspath()"
|
|
||||||
|
|
||||||
def test_return_bytes(self):
|
# Abstracted so it can be overridden to test pure Python implementation
|
||||||
for b in b'hello', b'goodbye', b'some/path/and/file':
|
# if a C version is provided.
|
||||||
self.assertEqual(b, os.fspath(b))
|
fspath = staticmethod(os.fspath)
|
||||||
|
|
||||||
def test_return_string(self):
|
|
||||||
for s in 'hello', 'goodbye', 'some/path/and/file':
|
|
||||||
self.assertEqual(s, os.fspath(s))
|
|
||||||
|
|
||||||
def test_fsencode_fsdecode_return_pathlike(self):
|
|
||||||
class PathLike:
|
|
||||||
def __init__(self, path):
|
|
||||||
self.path = path
|
|
||||||
def __fspath__(self):
|
|
||||||
return self.path
|
|
||||||
|
|
||||||
for p in "path/like/object", b"path/like/object":
|
|
||||||
pathlike = PathLike(p)
|
|
||||||
|
|
||||||
self.assertEqual(p, os.fspath(pathlike))
|
|
||||||
self.assertEqual(b"path/like/object", os.fsencode(pathlike))
|
|
||||||
self.assertEqual("path/like/object", os.fsdecode(pathlike))
|
|
||||||
|
|
||||||
def test_fspathlike(self):
|
|
||||||
class PathLike:
|
class PathLike:
|
||||||
def __init__(self, path=''):
|
def __init__(self, path=''):
|
||||||
self.path = path
|
self.path = path
|
||||||
def __fspath__(self):
|
def __fspath__(self):
|
||||||
return self.path
|
return self.path
|
||||||
|
|
||||||
self.assertEqual('#feelthegil', os.fspath(PathLike('#feelthegil')))
|
def test_return_bytes(self):
|
||||||
self.assertTrue(issubclass(PathLike, os.PathLike))
|
for b in b'hello', b'goodbye', b'some/path/and/file':
|
||||||
self.assertTrue(isinstance(PathLike(), os.PathLike))
|
self.assertEqual(b, self.fspath(b))
|
||||||
|
|
||||||
message = 'expected str, bytes or os.PathLike object, not'
|
def test_return_string(self):
|
||||||
for fn in (os.fsencode, os.fsdecode):
|
for s in 'hello', 'goodbye', 'some/path/and/file':
|
||||||
for obj in PathLike(None), None:
|
self.assertEqual(s, self.fspath(s))
|
||||||
with self.assertRaisesRegex(TypeError, message):
|
|
||||||
fn(obj)
|
def test_fsencode_fsdecode(self):
|
||||||
|
for p in "path/like/object", b"path/like/object":
|
||||||
|
pathlike = self.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))
|
||||||
|
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
self.fspath(self.PathLike(42))
|
||||||
|
|
||||||
def test_garbage_in_exception_out(self):
|
def test_garbage_in_exception_out(self):
|
||||||
vapor = type('blah', (), {})
|
vapor = type('blah', (), {})
|
||||||
for o in int, type, os, vapor():
|
for o in int, type, os, vapor():
|
||||||
self.assertRaises(TypeError, os.fspath, o)
|
self.assertRaises(TypeError, self.fspath, o)
|
||||||
|
|
||||||
def test_argument_required(self):
|
def test_argument_required(self):
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaises(TypeError):
|
||||||
os.fspath()
|
self.fspath()
|
||||||
|
|
||||||
|
|
||||||
|
# Only test if the C version is provided, otherwise TestPEP519 already tested
|
||||||
|
# the pure Python implementation.
|
||||||
|
if hasattr(os, "_fspath"):
|
||||||
|
class TestPEP519PurePython(TestPEP519):
|
||||||
|
|
||||||
|
"""Explicitly test the pure Python implementation of os.fspath()."""
|
||||||
|
|
||||||
|
fspath = staticmethod(os._fspath)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
@ -10,6 +10,9 @@ What's New in Python 3.6.0 alpha 3
|
||||||
Library
|
Library
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
- Issue #27186: Update os.fspath()/PyOS_FSPath() to check the return value of
|
||||||
|
__fspath__() to be either str or bytes.
|
||||||
|
|
||||||
- Issue #18726: All optional parameters of the dump(), dumps(),
|
- Issue #18726: All optional parameters of the dump(), dumps(),
|
||||||
load() and loads() functions and JSONEncoder and JSONDecoder class
|
load() and loads() functions and JSONEncoder and JSONDecoder class
|
||||||
constructors in the json module are now keyword-only.
|
constructors in the json module are now keyword-only.
|
||||||
|
|
|
@ -12317,12 +12317,21 @@ PyOS_FSPath(PyObject *path)
|
||||||
if (NULL == func) {
|
if (NULL == func) {
|
||||||
return PyErr_Format(PyExc_TypeError,
|
return PyErr_Format(PyExc_TypeError,
|
||||||
"expected str, bytes or os.PathLike object, "
|
"expected str, bytes or os.PathLike object, "
|
||||||
"not %S",
|
"not %.200s",
|
||||||
path->ob_type);
|
Py_TYPE(path)->tp_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
path_repr = PyObject_CallFunctionObjArgs(func, NULL);
|
path_repr = PyObject_CallFunctionObjArgs(func, NULL);
|
||||||
Py_DECREF(func);
|
Py_DECREF(func);
|
||||||
|
if (!(PyUnicode_Check(path_repr) || PyBytes_Check(path_repr))) {
|
||||||
|
PyErr_Format(PyExc_TypeError,
|
||||||
|
"expected %.200s.__fspath__() to return str or bytes, "
|
||||||
|
"not %.200s", Py_TYPE(path)->tp_name,
|
||||||
|
Py_TYPE(path_repr)->tp_name);
|
||||||
|
Py_DECREF(path_repr);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
return path_repr;
|
return path_repr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue