Skip to content

bpo-39899: Make pathlib use os.path.expanduser() to expand home directories #18841

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Doc/library/os.path.rst
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,8 @@ the :mod:`glob` module.)

On Windows, :envvar:`USERPROFILE` will be used if set, otherwise a combination
of :envvar:`HOMEPATH` and :envvar:`HOMEDRIVE` will be used. An initial
``~user`` is handled by stripping the last directory component from the created
user path derived above.
``~user`` is handled by checking that the last directory component of the current
user's home directory matches :envvar:`USERNAME`, and replacing it if so.

If the expansion fails or if the path does not begin with a tilde, the path is
returned unchanged.
Expand Down
10 changes: 8 additions & 2 deletions Doc/library/pathlib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -705,7 +705,10 @@ call fails (for example because the path doesn't exist).
.. classmethod:: Path.home()

Return a new path object representing the user's home directory (as
returned by :func:`os.path.expanduser` with ``~`` construct)::
returned by :func:`os.path.expanduser` with ``~`` construct). If the home
directory can't be resolved, :exc:`RuntimeError` is raised.

::

>>> Path.home()
PosixPath('/home/antoine')
Expand Down Expand Up @@ -773,7 +776,10 @@ call fails (for example because the path doesn't exist).
.. method:: Path.expanduser()

Return a new path with expanded ``~`` and ``~user`` constructs,
as returned by :meth:`os.path.expanduser`::
as returned by :meth:`os.path.expanduser`. If a home directory can't be
resolved, :exc:`RuntimeError` is raised.

::

>>> p = PosixPath('~/films/Monty Python')
>>> p.expanduser()
Expand Down
18 changes: 15 additions & 3 deletions Lib/ntpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,12 +312,24 @@ def expanduser(path):
drive = ''
userhome = join(drive, os.environ['HOMEPATH'])

if i != 1: #~user
# Try to guess user home directory. By default all users directories
# are located in the same place and are named by corresponding
# usernames. If current user home directory points to nonstandard
# place, this guess is likely wrong, and so we bail out.
current_user = os.environ.get('USERNAME')
if current_user != basename(userhome):
return path

target_user = path[1:i]
if isinstance(target_user, bytes):
target_user = os.fsdecode(target_user)
if target_user != current_user:
userhome = join(dirname(userhome), target_user)

if isinstance(path, bytes):
userhome = os.fsencode(userhome)

if i != 1: #~user
userhome = join(dirname(userhome), path[1:i])

return userhome + path[i:]


Expand Down
51 changes: 6 additions & 45 deletions Lib/pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,34 +246,6 @@ def make_uri(self, path):
# It's a path on a network drive => 'file://host/share/a/b'
return 'file:' + urlquote_from_bytes(path.as_posix().encode('utf-8'))

def gethomedir(self, username):
if 'USERPROFILE' in os.environ:
userhome = os.environ['USERPROFILE']
elif 'HOMEPATH' in os.environ:
try:
drv = os.environ['HOMEDRIVE']
except KeyError:
drv = ''
userhome = drv + os.environ['HOMEPATH']
else:
raise RuntimeError("Can't determine home directory")

if username:
# Try to guess user home directory. By default all users
# directories are located in the same place and are named by
# corresponding usernames. If current user home directory points
# to nonstandard place, this guess is likely wrong.
if os.environ['USERNAME'] != username:
drv, root, parts = self.parse_parts((userhome,))
if parts[-1] != os.environ['USERNAME']:
raise RuntimeError("Can't determine home directory "
"for %r" % username)
parts[-1] = username
if drv or root:
userhome = drv + root + self.join(parts[1:])
else:
userhome = self.join(parts)
return userhome

class _PosixFlavour(_Flavour):
sep = '/'
Expand Down Expand Up @@ -364,21 +336,6 @@ def make_uri(self, path):
bpath = bytes(path)
return 'file://' + urlquote_from_bytes(bpath)

def gethomedir(self, username):
if not username:
try:
return os.environ['HOME']
except KeyError:
import pwd
return pwd.getpwuid(os.getuid()).pw_dir
else:
import pwd
try:
return pwd.getpwnam(username).pw_dir
except KeyError:
raise RuntimeError("Can't determine home directory "
"for %r" % username)


_windows_flavour = _WindowsFlavour()
_posix_flavour = _PosixFlavour()
Expand Down Expand Up @@ -463,6 +420,8 @@ def group(self, path):

getcwd = os.getcwd

expanduser = staticmethod(os.path.expanduser)


_normal_accessor = _NormalAccessor()

Expand Down Expand Up @@ -1105,7 +1064,7 @@ def home(cls):
"""Return a new path pointing to the user's home directory (as
returned by os.path.expanduser('~')).
"""
return cls(cls()._flavour.gethomedir(None))
return cls("~").expanduser()

def samefile(self, other_path):
"""Return whether other_path is the same or not as this file
Expand Down Expand Up @@ -1517,7 +1476,9 @@ def expanduser(self):
"""
if (not (self._drv or self._root) and
self._parts and self._parts[0][:1] == '~'):
homedir = self._flavour.gethomedir(self._parts[0][1:])
homedir = self._accessor.expanduser(self._parts[0])
if homedir[:1] == "~":
raise RuntimeError("Could not determine home directory.")
return self._from_parts([homedir] + self._parts[1:])

return self
Expand Down
43 changes: 28 additions & 15 deletions Lib/test/test_ntpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -503,34 +503,47 @@ def test_expanduser(self):
env.clear()
tester('ntpath.expanduser("~test")', '~test')

env['HOMEPATH'] = 'eric\\idle'
env['HOMEDRIVE'] = 'C:\\'
tester('ntpath.expanduser("~test")', 'C:\\eric\\test')
tester('ntpath.expanduser("~")', 'C:\\eric\\idle')
env['HOMEPATH'] = 'Users\\eric'
env['USERNAME'] = 'eric'
tester('ntpath.expanduser("~test")', 'C:\\Users\\test')
tester('ntpath.expanduser("~")', 'C:\\Users\\eric')

del env['HOMEDRIVE']
tester('ntpath.expanduser("~test")', 'eric\\test')
tester('ntpath.expanduser("~")', 'eric\\idle')
tester('ntpath.expanduser("~test")', 'Users\\test')
tester('ntpath.expanduser("~")', 'Users\\eric')

env.clear()
env['USERPROFILE'] = 'C:\\eric\\idle'
tester('ntpath.expanduser("~test")', 'C:\\eric\\test')
tester('ntpath.expanduser("~")', 'C:\\eric\\idle')
env['USERPROFILE'] = 'C:\\Users\\eric'
env['USERNAME'] = 'eric'
tester('ntpath.expanduser("~test")', 'C:\\Users\\test')
tester('ntpath.expanduser("~")', 'C:\\Users\\eric')
tester('ntpath.expanduser("~test\\foo\\bar")',
'C:\\eric\\test\\foo\\bar')
'C:\\Users\\test\\foo\\bar')
tester('ntpath.expanduser("~test/foo/bar")',
'C:\\eric\\test/foo/bar')
'C:\\Users\\test/foo/bar')
tester('ntpath.expanduser("~\\foo\\bar")',
'C:\\eric\\idle\\foo\\bar')
'C:\\Users\\eric\\foo\\bar')
tester('ntpath.expanduser("~/foo/bar")',
'C:\\eric\\idle/foo/bar')
'C:\\Users\\eric/foo/bar')

# bpo-36264: ignore `HOME` when set on windows
env.clear()
env['HOME'] = 'F:\\'
env['USERPROFILE'] = 'C:\\eric\\idle'
tester('ntpath.expanduser("~test")', 'C:\\eric\\test')
tester('ntpath.expanduser("~")', 'C:\\eric\\idle')
env['USERPROFILE'] = 'C:\\Users\\eric'
env['USERNAME'] = 'eric'
tester('ntpath.expanduser("~test")', 'C:\\Users\\test')
tester('ntpath.expanduser("~")', 'C:\\Users\\eric')

# bpo-39899: don't guess another user's home directory if
# `%USERNAME% != basename(%USERPROFILE%)`
env.clear()
env['USERPROFILE'] = 'C:\\Users\\eric'
env['USERNAME'] = 'idle'
tester('ntpath.expanduser("~test")', '~test')
tester('ntpath.expanduser("~")', 'C:\\Users\\eric')



@unittest.skipUnless(nt, "abspath requires 'nt' module")
def test_abspath(self):
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -2609,7 +2609,7 @@ def check():
env.pop('USERNAME', None)
self.assertEqual(p1.expanduser(),
P('C:/Users/alice/My Documents'))
self.assertRaises(KeyError, p2.expanduser)
self.assertRaises(RuntimeError, p2.expanduser)
env['USERNAME'] = 'alice'
self.assertEqual(p2.expanduser(),
P('C:/Users/alice/My Documents'))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:func:`os.path.expanduser()` now refuses to guess Windows home directories if the basename of current user's home directory does not match their username.

:meth:`pathlib.Path.expanduser()` and :meth:`~pathlib.Path.home()` now consistently raise :exc:`RuntimeError` exception when a home directory cannot be resolved. Previously a :exc:`KeyError` exception could be raised on Windows when the ``"USERNAME"`` environment variable was unset.