Skip to content

Commit 0185f34

Browse files
bpo-33721: Make some os.path functions and pathlib.Path methods be tolerant to invalid paths. (#7695)
Such functions as os.path.exists(), os.path.lexists(), os.path.isdir(), os.path.isfile(), os.path.islink(), and os.path.ismount() now return False instead of raising ValueError or its subclasses UnicodeEncodeError and UnicodeDecodeError for paths that contain characters or bytes unrepresentative at the OS level.
1 parent 7bdf282 commit 0185f34

File tree

15 files changed

+181
-54
lines changed

15 files changed

+181
-54
lines changed

Doc/library/os.path.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@ the :mod:`glob` module.)
5555
* :mod:`macpath` for old-style MacOS paths
5656

5757

58+
.. versionchanged:: 3.8
59+
60+
:func:`exists`, :func:`lexists`, :func:`isdir`, :func:`isfile`,
61+
:func:`islink`, and :func:`ismount` now return ``False`` instead of
62+
raising an exception for paths that contain characters or bytes
63+
unrepresentable at the OS level.
64+
65+
5866
.. function:: abspath(path)
5967

6068
Return a normalized absolutized version of the pathname *path*. On most

Doc/library/pathlib.rst

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -638,7 +638,17 @@ Methods
638638

639639
Concrete paths provide the following methods in addition to pure paths
640640
methods. Many of these methods can raise an :exc:`OSError` if a system
641-
call fails (for example because the path doesn't exist):
641+
call fails (for example because the path doesn't exist).
642+
643+
.. versionchanged:: 3.8
644+
645+
:meth:`~Path.exists()`, :meth:`~Path.is_dir()`, :meth:`~Path.is_file()`,
646+
:meth:`~Path.is_mount()`, :meth:`~Path.is_symlink()`,
647+
:meth:`~Path.is_block_device()`, :meth:`~Path.is_char_device()`,
648+
:meth:`~Path.is_fifo()`, :meth:`~Path.is_socket()` now return ``False``
649+
instead of raising an exception for paths that contain characters
650+
unrepresentable at the OS level.
651+
642652

643653
.. classmethod:: Path.cwd()
644654

Doc/whatsnew/3.8.rst

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,31 @@ New Modules
112112
Improved Modules
113113
================
114114

115+
os.path
116+
-------
117+
118+
:mod:`os.path` functions that return a boolean result like
119+
:func:`~os.path.exists`, :func:`~os.path.lexists`, :func:`~os.path.isdir`,
120+
:func:`~os.path.isfile`, :func:`~os.path.islink`, and :func:`~os.path.ismount`
121+
now return ``False`` instead of raising :exc:`ValueError` or its subclasses
122+
:exc:`UnicodeEncodeError` and :exc:`UnicodeDecodeError` for paths that contain
123+
characters or bytes unrepresentable at the OS level.
124+
(Contributed by Serhiy Storchaka in :issue:`33721`.)
125+
126+
pathlib
127+
-------
128+
129+
:mod:`pathlib.Path` methods that return a boolean result like
130+
:meth:`~pathlib.Path.exists()`, :meth:`~pathlib.Path.is_dir()`,
131+
:meth:`~pathlib.Path.is_file()`, :meth:`~pathlib.Path.is_mount()`,
132+
:meth:`~pathlib.Path.is_symlink()`, :meth:`~pathlib.Path.is_block_device()`,
133+
:meth:`~pathlib.Path.is_char_device()`, :meth:`~pathlib.Path.is_fifo()`,
134+
:meth:`~pathlib.Path.is_socket()` now return ``False`` instead of raising
135+
:exc:`ValueError` or its subclass :exc:`UnicodeEncodeError` for paths that
136+
contain characters unrepresentable at the OS level.
137+
(Contributed by Serhiy Storchaka in :issue:`33721`.)
138+
139+
115140
Optimizations
116141
=============
117142

Lib/genericpath.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def exists(path):
1717
"""Test whether a path exists. Returns False for broken symbolic links"""
1818
try:
1919
os.stat(path)
20-
except OSError:
20+
except (OSError, ValueError):
2121
return False
2222
return True
2323

@@ -28,7 +28,7 @@ def isfile(path):
2828
"""Test whether a path is a regular file"""
2929
try:
3030
st = os.stat(path)
31-
except OSError:
31+
except (OSError, ValueError):
3232
return False
3333
return stat.S_ISREG(st.st_mode)
3434

@@ -40,7 +40,7 @@ def isdir(s):
4040
"""Return true if the pathname refers to an existing directory."""
4141
try:
4242
st = os.stat(s)
43-
except OSError:
43+
except (OSError, ValueError):
4444
return False
4545
return stat.S_ISDIR(st.st_mode)
4646

Lib/macpath.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ def lexists(path):
138138

139139
try:
140140
st = os.lstat(path)
141-
except OSError:
141+
except (OSError, ValueError):
142142
return False
143143
return True
144144

Lib/ntpath.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ def islink(path):
229229
"""
230230
try:
231231
st = os.lstat(path)
232-
except (OSError, AttributeError):
232+
except (OSError, ValueError, AttributeError):
233233
return False
234234
return stat.S_ISLNK(st.st_mode)
235235

@@ -239,7 +239,7 @@ def lexists(path):
239239
"""Test whether a path exists. Returns True for broken symbolic links"""
240240
try:
241241
st = os.lstat(path)
242-
except OSError:
242+
except (OSError, ValueError):
243243
return False
244244
return True
245245

@@ -524,7 +524,7 @@ def abspath(path):
524524
"""Return the absolute version of a path."""
525525
try:
526526
return _getfullpathname(path)
527-
except OSError:
527+
except (OSError, ValueError):
528528
return _abspath_fallback(path)
529529

530530
# realpath is a no-op on systems without islink support

Lib/pathlib.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1331,6 +1331,9 @@ def exists(self):
13311331
if e.errno not in _IGNORED_ERROS:
13321332
raise
13331333
return False
1334+
except ValueError:
1335+
# Non-encodable path
1336+
return False
13341337
return True
13351338

13361339
def is_dir(self):
@@ -1345,6 +1348,9 @@ def is_dir(self):
13451348
# Path doesn't exist or is a broken symlink
13461349
# (see https://bitbucket.org/pitrou/pathlib/issue/12/)
13471350
return False
1351+
except ValueError:
1352+
# Non-encodable path
1353+
return False
13481354

13491355
def is_file(self):
13501356
"""
@@ -1359,6 +1365,9 @@ def is_file(self):
13591365
# Path doesn't exist or is a broken symlink
13601366
# (see https://bitbucket.org/pitrou/pathlib/issue/12/)
13611367
return False
1368+
except ValueError:
1369+
# Non-encodable path
1370+
return False
13621371

13631372
def is_mount(self):
13641373
"""
@@ -1392,6 +1401,9 @@ def is_symlink(self):
13921401
raise
13931402
# Path doesn't exist
13941403
return False
1404+
except ValueError:
1405+
# Non-encodable path
1406+
return False
13951407

13961408
def is_block_device(self):
13971409
"""
@@ -1405,6 +1417,9 @@ def is_block_device(self):
14051417
# Path doesn't exist or is a broken symlink
14061418
# (see https://bitbucket.org/pitrou/pathlib/issue/12/)
14071419
return False
1420+
except ValueError:
1421+
# Non-encodable path
1422+
return False
14081423

14091424
def is_char_device(self):
14101425
"""
@@ -1418,6 +1433,9 @@ def is_char_device(self):
14181433
# Path doesn't exist or is a broken symlink
14191434
# (see https://bitbucket.org/pitrou/pathlib/issue/12/)
14201435
return False
1436+
except ValueError:
1437+
# Non-encodable path
1438+
return False
14211439

14221440
def is_fifo(self):
14231441
"""
@@ -1431,6 +1449,9 @@ def is_fifo(self):
14311449
# Path doesn't exist or is a broken symlink
14321450
# (see https://bitbucket.org/pitrou/pathlib/issue/12/)
14331451
return False
1452+
except ValueError:
1453+
# Non-encodable path
1454+
return False
14341455

14351456
def is_socket(self):
14361457
"""
@@ -1444,6 +1465,9 @@ def is_socket(self):
14441465
# Path doesn't exist or is a broken symlink
14451466
# (see https://bitbucket.org/pitrou/pathlib/issue/12/)
14461467
return False
1468+
except ValueError:
1469+
# Non-encodable path
1470+
return False
14471471

14481472
def expanduser(self):
14491473
""" Return a new path with expanded ~ and ~user constructs

Lib/posixpath.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ def islink(path):
169169
"""Test whether a path is a symbolic link"""
170170
try:
171171
st = os.lstat(path)
172-
except (OSError, AttributeError):
172+
except (OSError, ValueError, AttributeError):
173173
return False
174174
return stat.S_ISLNK(st.st_mode)
175175

@@ -179,7 +179,7 @@ def lexists(path):
179179
"""Test whether a path exists. Returns True for broken symbolic links"""
180180
try:
181181
os.lstat(path)
182-
except OSError:
182+
except (OSError, ValueError):
183183
return False
184184
return True
185185

@@ -191,7 +191,7 @@ def ismount(path):
191191
"""Test whether a path is a mount point"""
192192
try:
193193
s1 = os.lstat(path)
194-
except OSError:
194+
except (OSError, ValueError):
195195
# It doesn't exist -- so not a mount point. :-)
196196
return False
197197
else:
@@ -206,7 +206,7 @@ def ismount(path):
206206
parent = realpath(parent)
207207
try:
208208
s2 = os.lstat(parent)
209-
except OSError:
209+
except (OSError, ValueError):
210210
return False
211211

212212
dev1 = s1.st_dev

Lib/test/test_genericpath.py

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -138,10 +138,20 @@ def test_exists(self):
138138
self.assertIs(self.pathmodule.exists(filename), True)
139139
self.assertIs(self.pathmodule.exists(bfilename), True)
140140

141+
self.assertIs(self.pathmodule.exists(filename + '\udfff'), False)
142+
self.assertIs(self.pathmodule.exists(bfilename + b'\xff'), False)
143+
self.assertIs(self.pathmodule.exists(filename + '\x00'), False)
144+
self.assertIs(self.pathmodule.exists(bfilename + b'\x00'), False)
145+
141146
if self.pathmodule is not genericpath:
142147
self.assertIs(self.pathmodule.lexists(filename), True)
143148
self.assertIs(self.pathmodule.lexists(bfilename), True)
144149

150+
self.assertIs(self.pathmodule.lexists(filename + '\udfff'), False)
151+
self.assertIs(self.pathmodule.lexists(bfilename + b'\xff'), False)
152+
self.assertIs(self.pathmodule.lexists(filename + '\x00'), False)
153+
self.assertIs(self.pathmodule.lexists(bfilename + b'\x00'), False)
154+
145155
@unittest.skipUnless(hasattr(os, "pipe"), "requires os.pipe()")
146156
def test_exists_fd(self):
147157
r, w = os.pipe()
@@ -158,6 +168,11 @@ def test_isdir(self):
158168
self.assertIs(self.pathmodule.isdir(filename), False)
159169
self.assertIs(self.pathmodule.isdir(bfilename), False)
160170

171+
self.assertIs(self.pathmodule.isdir(filename + '\udfff'), False)
172+
self.assertIs(self.pathmodule.isdir(bfilename + b'\xff'), False)
173+
self.assertIs(self.pathmodule.isdir(filename + '\x00'), False)
174+
self.assertIs(self.pathmodule.isdir(bfilename + b'\x00'), False)
175+
161176
try:
162177
create_file(filename)
163178
self.assertIs(self.pathmodule.isdir(filename), False)
@@ -178,6 +193,11 @@ def test_isfile(self):
178193
self.assertIs(self.pathmodule.isfile(filename), False)
179194
self.assertIs(self.pathmodule.isfile(bfilename), False)
180195

196+
self.assertIs(self.pathmodule.isfile(filename + '\udfff'), False)
197+
self.assertIs(self.pathmodule.isfile(bfilename + b'\xff'), False)
198+
self.assertIs(self.pathmodule.isfile(filename + '\x00'), False)
199+
self.assertIs(self.pathmodule.isfile(bfilename + b'\x00'), False)
200+
181201
try:
182202
create_file(filename)
183203
self.assertIs(self.pathmodule.isfile(filename), True)
@@ -298,18 +318,20 @@ def test_invalid_paths(self):
298318
continue
299319
func = getattr(self.pathmodule, attr)
300320
with self.subTest(attr=attr):
301-
try:
321+
if attr in ('exists', 'isdir', 'isfile'):
302322
func('/tmp\udfffabcds')
303-
except (OSError, UnicodeEncodeError):
304-
pass
305-
try:
306323
func(b'/tmp\xffabcds')
307-
except (OSError, UnicodeDecodeError):
308-
pass
309-
with self.assertRaisesRegex(ValueError, 'embedded null'):
310324
func('/tmp\x00abcds')
311-
with self.assertRaisesRegex(ValueError, 'embedded null'):
312325
func(b'/tmp\x00abcds')
326+
else:
327+
with self.assertRaises((OSError, UnicodeEncodeError)):
328+
func('/tmp\udfffabcds')
329+
with self.assertRaises((OSError, UnicodeDecodeError)):
330+
func(b'/tmp\xffabcds')
331+
with self.assertRaisesRegex(ValueError, 'embedded null'):
332+
func('/tmp\x00abcds')
333+
with self.assertRaisesRegex(ValueError, 'embedded null'):
334+
func(b'/tmp\x00abcds')
313335

314336
# Following TestCase is not supposed to be run from test_genericpath.
315337
# It is inherited by other test modules (macpath, ntpath, posixpath).

0 commit comments

Comments
 (0)