Skip to content

Commit ec6ce87

Browse files
committed
Issue #26027: Support path-like objects in PyUnicode-FSConverter().
This is to add support for os.exec*() and os.spawn*() functions. Part of PEP 519.
1 parent dc5a3fe commit ec6ce87

4 files changed

Lines changed: 50 additions & 49 deletions

File tree

Doc/c-api/unicode.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -810,13 +810,16 @@ used, passing :c:func:`PyUnicode_FSConverter` as the conversion function:
810810
811811
.. c:function:: int PyUnicode_FSConverter(PyObject* obj, void* result)
812812
813-
ParseTuple converter: encode :class:`str` objects to :class:`bytes` using
813+
ParseTuple converter: encode :class:`str` objects -- obtained directly or
814+
through the :class:`os.PathLike` interface -- to :class:`bytes` using
814815
:c:func:`PyUnicode_EncodeFSDefault`; :class:`bytes` objects are output as-is.
815816
*result* must be a :c:type:`PyBytesObject*` which must be released when it is
816817
no longer used.
817818
818819
.. versionadded:: 3.1
819820
821+
.. versionchanged:: 3.6
822+
Accepts a :term:`path-like object`.
820823
821824
To decode file names during argument parsing, the ``"O&"`` converter should be
822825
used, passing :c:func:`PyUnicode_FSDecoder` as the conversion function:

Lib/test/test_os.py

Lines changed: 28 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,21 @@ def bytes_filename_warn(expected):
100100
yield
101101

102102

103+
class _PathLike(os.PathLike):
104+
105+
def __init__(self, path=""):
106+
self.path = path
107+
108+
def __str__(self):
109+
return str(self.path)
110+
111+
def __fspath__(self):
112+
if isinstance(self.path, BaseException):
113+
raise self.path
114+
else:
115+
return self.path
116+
117+
103118
def create_file(filename, content=b'content'):
104119
with open(filename, "xb", 0) as fp:
105120
fp.write(content)
@@ -894,15 +909,7 @@ def test_walk_prune(self, walk_path=None):
894909
self.assertEqual(all[1], self.sub2_tree)
895910

896911
def test_file_like_path(self):
897-
class FileLike:
898-
def __init__(self, path):
899-
self._path = path
900-
def __str__(self):
901-
return str(self._path)
902-
def __fspath__(self):
903-
return self._path
904-
905-
self.test_walk_prune(FileLike(self.walk_path))
912+
self.test_walk_prune(_PathLike(self.walk_path))
906913

907914
def test_walk_bottom_up(self):
908915
# Walk bottom-up.
@@ -2124,7 +2131,8 @@ def test_getppid(self):
21242131

21252132
def test_waitpid(self):
21262133
args = [sys.executable, '-c', 'pass']
2127-
pid = os.spawnv(os.P_NOWAIT, args[0], args)
2134+
# Add an implicit test for PyUnicode_FSConverter().
2135+
pid = os.spawnv(os.P_NOWAIT, _PathLike(args[0]), args)
21282136
status = os.waitpid(pid, 0)
21292137
self.assertEqual(status, (pid, 0))
21302138

@@ -2833,25 +2841,18 @@ class PathTConverterTests(unittest.TestCase):
28332841
]
28342842

28352843
def test_path_t_converter(self):
2836-
class PathLike:
2837-
def __init__(self, path):
2838-
self.path = path
2839-
2840-
def __fspath__(self):
2841-
return self.path
2842-
28432844
str_filename = support.TESTFN
28442845
if os.name == 'nt':
28452846
bytes_fspath = bytes_filename = None
28462847
else:
28472848
bytes_filename = support.TESTFN.encode('ascii')
2848-
bytes_fspath = PathLike(bytes_filename)
2849-
fd = os.open(PathLike(str_filename), os.O_WRONLY|os.O_CREAT)
2849+
bytes_fspath = _PathLike(bytes_filename)
2850+
fd = os.open(_PathLike(str_filename), os.O_WRONLY|os.O_CREAT)
28502851
self.addCleanup(support.unlink, support.TESTFN)
28512852
self.addCleanup(os.close, fd)
28522853

2853-
int_fspath = PathLike(fd)
2854-
str_fspath = PathLike(str_filename)
2854+
int_fspath = _PathLike(fd)
2855+
str_fspath = _PathLike(str_filename)
28552856

28562857
for name, allow_fd, extra_args, cleanup_fn in self.functions:
28572858
with self.subTest(name=name):
@@ -3205,15 +3206,6 @@ class TestPEP519(unittest.TestCase):
32053206
# if a C version is provided.
32063207
fspath = staticmethod(os.fspath)
32073208

3208-
class PathLike:
3209-
def __init__(self, path=''):
3210-
self.path = path
3211-
def __fspath__(self):
3212-
if isinstance(self.path, BaseException):
3213-
raise self.path
3214-
else:
3215-
return self.path
3216-
32173209
def test_return_bytes(self):
32183210
for b in b'hello', b'goodbye', b'some/path/and/file':
32193211
self.assertEqual(b, self.fspath(b))
@@ -3224,16 +3216,16 @@ def test_return_string(self):
32243216

32253217
def test_fsencode_fsdecode(self):
32263218
for p in "path/like/object", b"path/like/object":
3227-
pathlike = self.PathLike(p)
3219+
pathlike = _PathLike(p)
32283220

32293221
self.assertEqual(p, self.fspath(pathlike))
32303222
self.assertEqual(b"path/like/object", os.fsencode(pathlike))
32313223
self.assertEqual("path/like/object", os.fsdecode(pathlike))
32323224

32333225
def test_pathlike(self):
3234-
self.assertEqual('#feelthegil', self.fspath(self.PathLike('#feelthegil')))
3235-
self.assertTrue(issubclass(self.PathLike, os.PathLike))
3236-
self.assertTrue(isinstance(self.PathLike(), os.PathLike))
3226+
self.assertEqual('#feelthegil', self.fspath(_PathLike('#feelthegil')))
3227+
self.assertTrue(issubclass(_PathLike, os.PathLike))
3228+
self.assertTrue(isinstance(_PathLike(), os.PathLike))
32373229

32383230
def test_garbage_in_exception_out(self):
32393231
vapor = type('blah', (), {})
@@ -3245,14 +3237,14 @@ def test_argument_required(self):
32453237

32463238
def test_bad_pathlike(self):
32473239
# __fspath__ returns a value other than str or bytes.
3248-
self.assertRaises(TypeError, self.fspath, self.PathLike(42))
3240+
self.assertRaises(TypeError, self.fspath, _PathLike(42))
32493241
# __fspath__ attribute that is not callable.
32503242
c = type('foo', (), {})
32513243
c.__fspath__ = 1
32523244
self.assertRaises(TypeError, self.fspath, c())
32533245
# __fspath__ raises an exception.
32543246
self.assertRaises(ZeroDivisionError, self.fspath,
3255-
self.PathLike(ZeroDivisionError()))
3247+
_PathLike(ZeroDivisionError()))
32563248

32573249
# Only test if the C version is provided, otherwise TestPEP519 already tested
32583250
# the pure Python implementation.

Misc/NEWS

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,11 @@ Library
188188

189189
- Issue #27573: exit message for code.interact is now configurable.
190190

191+
C API
192+
-----
193+
194+
- Issue #26027: Add support for path-like objects in PyUnicode_FSConverter().
195+
191196
Tests
192197
-----
193198

Objects/unicodeobject.c

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3842,6 +3842,7 @@ PyUnicode_DecodeFSDefaultAndSize(const char *s, Py_ssize_t size)
38423842
int
38433843
PyUnicode_FSConverter(PyObject* arg, void* addr)
38443844
{
3845+
PyObject *path = NULL;
38453846
PyObject *output = NULL;
38463847
Py_ssize_t size;
38473848
void *data;
@@ -3850,22 +3851,22 @@ PyUnicode_FSConverter(PyObject* arg, void* addr)
38503851
*(PyObject**)addr = NULL;
38513852
return 1;
38523853
}
3853-
if (PyBytes_Check(arg)) {
3854-
output = arg;
3855-
Py_INCREF(output);
3854+
path = PyOS_FSPath(arg);
3855+
if (path == NULL) {
3856+
return 0;
38563857
}
3857-
else if (PyUnicode_Check(arg)) {
3858-
output = PyUnicode_EncodeFSDefault(arg);
3859-
if (!output)
3858+
if (PyBytes_Check(path)) {
3859+
output = path;
3860+
}
3861+
else { // PyOS_FSPath() guarantees its returned value is bytes or str.
3862+
output = PyUnicode_EncodeFSDefault(path);
3863+
Py_DECREF(path);
3864+
if (!output) {
38603865
return 0;
3866+
}
38613867
assert(PyBytes_Check(output));
38623868
}
3863-
else {
3864-
PyErr_Format(PyExc_TypeError,
3865-
"must be str or bytes, not %.100s",
3866-
Py_TYPE(arg)->tp_name);
3867-
return 0;
3868-
}
3869+
38693870
size = PyBytes_GET_SIZE(output);
38703871
data = PyBytes_AS_STRING(output);
38713872
if ((size_t)size != strlen(data)) {

0 commit comments

Comments
 (0)