diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index b6507eb4d6fa2c..fdb5c53e20bf24 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -702,19 +702,25 @@ call fails (for example because the path doesn't exist). PosixPath('/home/antoine/pathlib') -.. classmethod:: Path.home() +.. classmethod:: Path.home(user=None) - Return a new path object representing the user's home directory (as + Return a new path object representing a user's home directory (as returned by :func:`os.path.expanduser` with ``~`` construct). If the home - directory can't be resolved, :exc:`RuntimeError` is raised. + directory can't be resolved, :exc:`RuntimeError` is raised. If no user name + is given, the current user's home directory is used. :: >>> Path.home() PosixPath('/home/antoine') + >>> Path.home('barney') + PosixPath('/home/barney') .. versionadded:: 3.5 + .. versionchanged:: 3.10 + The *user* parameter was added. + .. method:: Path.stat(*, follow_symlinks=True) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index 37934c6038e1d1..e8dea2da08d1ba 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -1055,14 +1055,22 @@ def cwd(cls): """Return a new path pointing to the current working directory (as returned by os.getcwd()). """ - return cls(cls()._accessor.getcwd()) + return cls(cls._accessor.getcwd()) @classmethod - def home(cls): + def home(cls, user=None): """Return a new path pointing to the user's home directory (as returned by os.path.expanduser('~')). """ - return cls("~").expanduser() + tilde = "~" + if user is None: + user = "" + elif isinstance(user, bytes): + tilde = b"~" + homedir = cls._accessor.expanduser(tilde + user) + if homedir[:1] == tilde: + raise RuntimeError("Could not determine home directory.") + return cls(homedir) def samefile(self, other_path): """Return whether other_path is the same or not as this file @@ -1488,9 +1496,7 @@ def expanduser(self): """ if (not (self._drv or self._root) and self._parts and self._parts[0][:1] == '~'): - homedir = self._accessor.expanduser(self._parts[0]) - if homedir[:1] == "~": - raise RuntimeError("Could not determine home directory.") + homedir = self.home(self.parts[0][1:]) return self._from_parts([homedir] + self._parts[1:]) return self diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index 6ed08f7e70ce3d..ce0aabfb9e3282 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -2559,6 +2559,42 @@ def test_expanduser(self): self.assertEqual(p6.expanduser(), p6) self.assertRaises(RuntimeError, p7.expanduser) + @unittest.skipUnless(hasattr(pwd, 'getpwall'), + 'pwd module does not expose getpwall()') + @unittest.skipIf(sys.platform == "vxworks", + "no home directory on VxWorks") + def test_home(self): + P = self.cls + import_helper.import_module('pwd') + import pwd + pwdent = pwd.getpwuid(os.getuid()) + username = pwdent.pw_name + userhome = pwdent.pw_dir.rstrip('/') or '/' + # Find arbitrary different user (if exists). + for pwdent in pwd.getpwall(): + othername = pwdent.pw_name + otherhome = pwdent.pw_dir.rstrip('/') + if othername != username and otherhome: + break + else: + othername = username + otherhome = userhome + + with os_helper.EnvironmentVarGuard() as env: + env.pop('HOME', None) + + self.assertEqual(P.home(), P(userhome)) + self.assertEqual(P.home(username), P(userhome)) + self.assertEqual(P.home(othername), P(otherhome)) + self.assertRaises(RuntimeError, P.home, 'missinguser') + + env['HOME'] = '/tmp' + + self.assertEqual(P.home(), P('/tmp')) + self.assertEqual(P.home(username), P(userhome)) + self.assertEqual(P.home(othername), P(otherhome)) + self.assertRaises(RuntimeError, P.home, 'missinguser') + @unittest.skipIf(sys.platform != "darwin", "Bad file descriptor in /dev/fd affects only macOS") def test_handling_bad_descriptor(self): @@ -2654,6 +2690,43 @@ def check(): env['HOME'] = 'C:\\Users\\eve' check() + def test_home(self): + P = self.cls + with os_helper.EnvironmentVarGuard() as env: + env.pop('HOME', None) + env.pop('USERPROFILE', None) + env.pop('HOMEPATH', None) + env.pop('HOMEDRIVE', None) + env['USERNAME'] = 'alice' + + self.assertRaises(RuntimeError, P.home) + self.assertRaises(RuntimeError, P.home, 'alice') + self.assertRaises(RuntimeError, P.home, 'bob') + + def check(): + env.pop('USERNAME', None) + self.assertEqual(P.home(), P('C:/Users/alice')) + self.assertRaises(RuntimeError, P.home, 'alice') + env['USERNAME'] = 'alice' + self.assertEqual(P.home('alice'), P('C:/Users/alice')) + self.assertEqual(P.home('bob'), P('C:/Users/bob')) + + env['HOMEPATH'] = 'C:\\Users\\alice' + check() + + env['HOMEDRIVE'] = 'C:\\' + env['HOMEPATH'] = 'Users\\alice' + check() + + env.pop('HOMEDRIVE', None) + env.pop('HOMEPATH', None) + env['USERPROFILE'] = 'C:\\Users\\alice' + check() + + # bpo-38883: ignore `HOME` when set on windows + env['HOME'] = 'C:\\Users\\eve' + check() + class CompatiblePathTest(unittest.TestCase): """ diff --git a/Misc/NEWS.d/next/Library/2021-04-08-20-07-43.bpo-25271.c27ipG.rst b/Misc/NEWS.d/next/Library/2021-04-08-20-07-43.bpo-25271.c27ipG.rst new file mode 100644 index 00000000000000..88e6bd03a57284 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-04-08-20-07-43.bpo-25271.c27ipG.rst @@ -0,0 +1 @@ +Add *user* parameter to `pathlib.Path.home()`