bpo-10496: posixpath.expanduser() catchs pwd.getpwuid() error (GH-10919)

* 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 f2f4555d82)

Co-authored-by: Victor Stinner <vstinner@redhat.com>
This commit is contained in:
Miss Islington (bot) 2018-12-05 08:07:57 -08:00 committed by GitHub
parent 2d594f8578
commit 983d1ab0e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 98 additions and 29 deletions

View File

@ -246,7 +246,12 @@ def expanduser(path):
if i == 1: if i == 1:
if 'HOME' not in os.environ: if 'HOME' not in os.environ:
import pwd import pwd
try:
userhome = pwd.getpwuid(os.getuid()).pw_dir userhome = pwd.getpwuid(os.getuid()).pw_dir
except KeyError:
# bpo-10496: if the current user identifier doesn't exist in the
# password database, return the path unchanged
return path
else: else:
userhome = os.environ['HOME'] userhome = os.environ['HOME']
else: else:
@ -257,6 +262,8 @@ def expanduser(path):
try: try:
pwent = pwd.getpwnam(name) pwent = pwd.getpwnam(name)
except KeyError: except KeyError:
# bpo-10496: if the user name from the path doesn't exist in the
# password database, return the path unchanged
return path return path
userhome = pwent.pw_dir userhome = pwent.pw_dir
if isinstance(path, bytes): if isinstance(path, bytes):

View File

@ -5,6 +5,7 @@ import warnings
from posixpath import realpath, abspath, dirname, basename from posixpath import realpath, abspath, dirname, basename
from test import support, test_genericpath from test import support, test_genericpath
from test.support import FakePath from test.support import FakePath
from unittest import mock
try: try:
import posix import posix
@ -230,20 +231,29 @@ class PosixPathTest(unittest.TestCase):
def test_expanduser(self): def test_expanduser(self):
self.assertEqual(posixpath.expanduser("foo"), "foo") self.assertEqual(posixpath.expanduser("foo"), "foo")
self.assertEqual(posixpath.expanduser(b"foo"), b"foo") self.assertEqual(posixpath.expanduser(b"foo"), b"foo")
def test_expanduser_home_envvar(self):
with support.EnvironmentVarGuard() as env: with support.EnvironmentVarGuard() as env:
env['HOME'] = '/home/victor'
self.assertEqual(posixpath.expanduser("~"), "/home/victor")
# expanduser() strips trailing slash
env['HOME'] = '/home/victor/'
self.assertEqual(posixpath.expanduser("~"), "/home/victor")
for home in '/', '', '//', '///': for home in '/', '', '//', '///':
with self.subTest(home=home): with self.subTest(home=home):
env['HOME'] = home env['HOME'] = home
self.assertEqual(posixpath.expanduser("~"), "/") self.assertEqual(posixpath.expanduser("~"), "/")
self.assertEqual(posixpath.expanduser("~/"), "/") self.assertEqual(posixpath.expanduser("~/"), "/")
self.assertEqual(posixpath.expanduser("~/foo"), "/foo") self.assertEqual(posixpath.expanduser("~/foo"), "/foo")
try:
import pwd def test_expanduser_pwd(self):
except ImportError: pwd = support.import_module('pwd')
pass
else:
self.assertIsInstance(posixpath.expanduser("~/"), str) self.assertIsInstance(posixpath.expanduser("~/"), str)
self.assertIsInstance(posixpath.expanduser(b"~/"), bytes) self.assertIsInstance(posixpath.expanduser(b"~/"), bytes)
# if home directory == root directory, this test makes no sense # if home directory == root directory, this test makes no sense
if posixpath.expanduser("~") != '/': if posixpath.expanduser("~") != '/':
self.assertEqual( self.assertEqual(
@ -262,11 +272,21 @@ class PosixPathTest(unittest.TestCase):
with support.EnvironmentVarGuard() as env: with support.EnvironmentVarGuard() as env:
# expanduser should fall back to using the password database # expanduser should fall back to using the password database
del env['HOME'] del env['HOME']
home = pwd.getpwuid(os.getuid()).pw_dir home = pwd.getpwuid(os.getuid()).pw_dir
# $HOME can end with a trailing /, so strip it (see #17809) # $HOME can end with a trailing /, so strip it (see #17809)
home = home.rstrip("/") or '/' home = home.rstrip("/") or '/'
self.assertEqual(posixpath.expanduser("~"), home) self.assertEqual(posixpath.expanduser("~"), home)
# bpo-10496: If the HOME environment variable is not set and the
# user (current identifier or name in the path) doesn't exist in
# the password database (pwd.getuid() or pwd.getpwnam() fail),
# expanduser() must return the path unchanged.
with mock.patch.object(pwd, 'getpwuid', side_effect=KeyError), \
mock.patch.object(pwd, 'getpwnam', side_effect=KeyError):
for path in ('~', '~/.local', '~vstinner/'):
self.assertEqual(posixpath.expanduser(path), path)
def test_normpath(self): def test_normpath(self):
self.assertEqual(posixpath.normpath(""), ".") self.assertEqual(posixpath.normpath(""), ".")
self.assertEqual(posixpath.normpath("/"), "/") self.assertEqual(posixpath.normpath("/"), "/")

View File

@ -6,6 +6,7 @@ executing have not been removed.
""" """
import unittest import unittest
import test.support import test.support
from test import support
from test.support import (captured_stderr, TESTFN, EnvironmentVarGuard, from test.support import (captured_stderr, TESTFN, EnvironmentVarGuard,
change_cwd) change_cwd)
import builtins import builtins
@ -19,6 +20,7 @@ import shutil
import subprocess import subprocess
import sysconfig import sysconfig
import tempfile import tempfile
from unittest import mock
from copy import copy from copy import copy
# These tests are not particularly useful if Python was invoked with -S. # These tests are not particularly useful if Python was invoked with -S.
@ -258,6 +260,7 @@ class HelperFunctionsTests(unittest.TestCase):
# the call sets USER_BASE *and* USER_SITE # the call sets USER_BASE *and* USER_SITE
self.assertEqual(site.USER_SITE, user_site) self.assertEqual(site.USER_SITE, user_site)
self.assertTrue(user_site.startswith(site.USER_BASE), user_site) self.assertTrue(user_site.startswith(site.USER_BASE), user_site)
self.assertEqual(site.USER_BASE, site.getuserbase())
def test_getsitepackages(self): def test_getsitepackages(self):
site.PREFIXES = ['xoxo'] site.PREFIXES = ['xoxo']
@ -276,6 +279,40 @@ class HelperFunctionsTests(unittest.TestCase):
wanted = os.path.join('xoxo', 'lib', 'site-packages') wanted = os.path.join('xoxo', 'lib', 'site-packages')
self.assertEqual(dirs[1], wanted) self.assertEqual(dirs[1], wanted)
def test_no_home_directory(self):
# bpo-10496: getuserbase() and getusersitepackages() must not fail if
# the current user has no home directory (if expanduser() returns the
# path unchanged).
site.USER_SITE = None
site.USER_BASE = None
with EnvironmentVarGuard() as environ, \
mock.patch('os.path.expanduser', lambda path: path):
del environ['PYTHONUSERBASE']
del environ['APPDATA']
user_base = site.getuserbase()
self.assertTrue(user_base.startswith('~' + os.sep),
user_base)
user_site = site.getusersitepackages()
self.assertTrue(user_site.startswith(user_base), user_site)
with mock.patch('os.path.isdir', return_value=False) as mock_isdir, \
mock.patch.object(site, 'addsitedir') as mock_addsitedir, \
support.swap_attr(site, 'ENABLE_USER_SITE', True):
# addusersitepackages() must not add user_site to sys.path
# if it is not an existing directory
known_paths = set()
site.addusersitepackages(known_paths)
mock_isdir.assert_called_once_with(user_site)
mock_addsitedir.assert_not_called()
self.assertFalse(known_paths)
class PthFile(object): class PthFile(object):
"""Helper class for handling testing of .pth files""" """Helper class for handling testing of .pth files"""

View File

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