Skip to content

Commit 31b635d

Browse files
authored
bpo-10496: posixpath.expanduser() catchs pwd.getpwuid() error (GH-10919) (GH-10925)
* posixpath.expanduser() now returns the input path unchanged if the HOME environment variable is not set and pwd.getpwuid() raises KeyError (the current user identifier doesn't exist in the password database). * Add test_no_home_directory() to test_site. (cherry picked from commit f2f4555)
1 parent bfb8818 commit 31b635d

4 files changed

Lines changed: 100 additions & 30 deletions

File tree

Lib/posixpath.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,12 @@ def expanduser(path):
246246
if i == 1:
247247
if 'HOME' not in os.environ:
248248
import pwd
249-
userhome = pwd.getpwuid(os.getuid()).pw_dir
249+
try:
250+
userhome = pwd.getpwuid(os.getuid()).pw_dir
251+
except KeyError:
252+
# bpo-10496: if the current user identifier doesn't exist in the
253+
# password database, return the path unchanged
254+
return path
250255
else:
251256
userhome = os.environ['HOME']
252257
else:
@@ -257,6 +262,8 @@ def expanduser(path):
257262
try:
258263
pwent = pwd.getpwnam(name)
259264
except KeyError:
265+
# bpo-10496: if the user name from the path doesn't exist in the
266+
# password database, return the path unchanged
260267
return path
261268
userhome = pwent.pw_dir
262269
if isinstance(path, bytes):

Lib/test/test_posixpath.py

Lines changed: 49 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from posixpath import realpath, abspath, dirname, basename
66
from test import support, test_genericpath
77
from test.support import FakePath
8+
from unittest import mock
89

910
try:
1011
import posix
@@ -230,42 +231,61 @@ def fake_lstat(path):
230231
def test_expanduser(self):
231232
self.assertEqual(posixpath.expanduser("foo"), "foo")
232233
self.assertEqual(posixpath.expanduser(b"foo"), b"foo")
234+
235+
def test_expanduser_home_envvar(self):
233236
with support.EnvironmentVarGuard() as env:
237+
env['HOME'] = '/home/victor'
238+
self.assertEqual(posixpath.expanduser("~"), "/home/victor")
239+
240+
# expanduser() strips trailing slash
241+
env['HOME'] = '/home/victor/'
242+
self.assertEqual(posixpath.expanduser("~"), "/home/victor")
243+
234244
for home in '/', '', '//', '///':
235245
with self.subTest(home=home):
236246
env['HOME'] = home
237247
self.assertEqual(posixpath.expanduser("~"), "/")
238248
self.assertEqual(posixpath.expanduser("~/"), "/")
239249
self.assertEqual(posixpath.expanduser("~/foo"), "/foo")
240-
try:
241-
import pwd
242-
except ImportError:
243-
pass
244-
else:
245-
self.assertIsInstance(posixpath.expanduser("~/"), str)
246-
self.assertIsInstance(posixpath.expanduser(b"~/"), bytes)
247-
# if home directory == root directory, this test makes no sense
248-
if posixpath.expanduser("~") != '/':
249-
self.assertEqual(
250-
posixpath.expanduser("~") + "/",
251-
posixpath.expanduser("~/")
252-
)
253-
self.assertEqual(
254-
posixpath.expanduser(b"~") + b"/",
255-
posixpath.expanduser(b"~/")
256-
)
257-
self.assertIsInstance(posixpath.expanduser("~root/"), str)
258-
self.assertIsInstance(posixpath.expanduser("~foo/"), str)
259-
self.assertIsInstance(posixpath.expanduser(b"~root/"), bytes)
260-
self.assertIsInstance(posixpath.expanduser(b"~foo/"), bytes)
261-
262-
with support.EnvironmentVarGuard() as env:
263-
# expanduser should fall back to using the password database
264-
del env['HOME']
265-
home = pwd.getpwuid(os.getuid()).pw_dir
266-
# $HOME can end with a trailing /, so strip it (see #17809)
267-
home = home.rstrip("/") or '/'
268-
self.assertEqual(posixpath.expanduser("~"), home)
250+
251+
def test_expanduser_pwd(self):
252+
pwd = support.import_module('pwd')
253+
254+
self.assertIsInstance(posixpath.expanduser("~/"), str)
255+
self.assertIsInstance(posixpath.expanduser(b"~/"), bytes)
256+
257+
# if home directory == root directory, this test makes no sense
258+
if posixpath.expanduser("~") != '/':
259+
self.assertEqual(
260+
posixpath.expanduser("~") + "/",
261+
posixpath.expanduser("~/")
262+
)
263+
self.assertEqual(
264+
posixpath.expanduser(b"~") + b"/",
265+
posixpath.expanduser(b"~/")
266+
)
267+
self.assertIsInstance(posixpath.expanduser("~root/"), str)
268+
self.assertIsInstance(posixpath.expanduser("~foo/"), str)
269+
self.assertIsInstance(posixpath.expanduser(b"~root/"), bytes)
270+
self.assertIsInstance(posixpath.expanduser(b"~foo/"), bytes)
271+
272+
with support.EnvironmentVarGuard() as env:
273+
# expanduser should fall back to using the password database
274+
del env['HOME']
275+
276+
home = pwd.getpwuid(os.getuid()).pw_dir
277+
# $HOME can end with a trailing /, so strip it (see #17809)
278+
home = home.rstrip("/") or '/'
279+
self.assertEqual(posixpath.expanduser("~"), home)
280+
281+
# bpo-10496: If the HOME environment variable is not set and the
282+
# user (current identifier or name in the path) doesn't exist in
283+
# the password database (pwd.getuid() or pwd.getpwnam() fail),
284+
# expanduser() must return the path unchanged.
285+
with mock.patch.object(pwd, 'getpwuid', side_effect=KeyError), \
286+
mock.patch.object(pwd, 'getpwnam', side_effect=KeyError):
287+
for path in ('~', '~/.local', '~vstinner/'):
288+
self.assertEqual(posixpath.expanduser(path), path)
269289

270290
def test_normpath(self):
271291
self.assertEqual(posixpath.normpath(""), ".")

Lib/test/test_site.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"""
77
import unittest
88
import test.support
9+
from test import support
910
from test.support import (captured_stderr, TESTFN, EnvironmentVarGuard,
1011
change_cwd)
1112
import builtins
@@ -18,6 +19,7 @@
1819
import shutil
1920
import subprocess
2021
import sysconfig
22+
from unittest import mock
2123
from copy import copy
2224

2325
# These tests are not particularly useful if Python was invoked with -S.
@@ -243,6 +245,7 @@ def test_getusersitepackages(self):
243245
# the call sets USER_BASE *and* USER_SITE
244246
self.assertEqual(site.USER_SITE, user_site)
245247
self.assertTrue(user_site.startswith(site.USER_BASE), user_site)
248+
self.assertEqual(site.USER_BASE, site.getuserbase())
246249

247250
def test_getsitepackages(self):
248251
site.PREFIXES = ['xoxo']
@@ -273,6 +276,41 @@ def test_getsitepackages(self):
273276
wanted = os.path.join('xoxo', 'lib', 'site-packages')
274277
self.assertEqual(dirs[1], wanted)
275278

279+
def test_no_home_directory(self):
280+
# bpo-10496: getuserbase() and getusersitepackages() must not fail if
281+
# the current user has no home directory (if expanduser() returns the
282+
# path unchanged).
283+
site.USER_SITE = None
284+
site.USER_BASE = None
285+
sysconfig._CONFIG_VARS = None
286+
287+
with EnvironmentVarGuard() as environ, \
288+
mock.patch('os.path.expanduser', lambda path: path):
289+
290+
del environ['PYTHONUSERBASE']
291+
del environ['APPDATA']
292+
293+
user_base = site.getuserbase()
294+
self.assertTrue(user_base.startswith('~' + os.sep),
295+
user_base)
296+
297+
user_site = site.getusersitepackages()
298+
self.assertTrue(user_site.startswith(user_base), user_site)
299+
300+
with mock.patch('os.path.isdir', return_value=False) as mock_isdir, \
301+
mock.patch.object(site, 'addsitedir') as mock_addsitedir, \
302+
support.swap_attr(site, 'ENABLE_USER_SITE', True):
303+
304+
# addusersitepackages() must not add user_site to sys.path
305+
# if it is not an existing directory
306+
known_paths = set()
307+
site.addusersitepackages(known_paths)
308+
309+
mock_isdir.assert_called_once_with(user_site)
310+
mock_addsitedir.assert_not_called()
311+
self.assertFalse(known_paths)
312+
313+
276314
class PthFile(object):
277315
"""Helper class for handling testing of .pth files"""
278316

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
:func:`posixpath.expanduser` now returns the input *path* unchanged if the
2+
``HOME`` environment variable is not set and the current user has no home
3+
directory (if the current user identifier doesn't exist in the password
4+
database). This change fix the :mod:`site` module if the current user doesn't
5+
exist in the password database (if the user has no home directory).

0 commit comments

Comments
 (0)