From 621f631383840b2bcd919b52453592d64c7a8f7c Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Sun, 20 Apr 2025 20:25:56 +0100 Subject: [PATCH 1/7] Expand ~ --- Lib/glob.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Lib/glob.py b/Lib/glob.py index 8879eff80415aa..61f2a0a43fd73a 100644 --- a/Lib/glob.py +++ b/Lib/glob.py @@ -45,6 +45,10 @@ def iglob(pathname, *, root_dir=None, dir_fd=None, recursive=False, """ sys.audit("glob.glob", pathname, recursive) sys.audit("glob.glob/2", pathname, recursive, root_dir, dir_fd) + # expand ~; see issue 84037 + if pathname.startswith('~'): + from pathlib import Path + pathname = pathname.replace('~', str(Path.home()), 1) if root_dir is not None: root_dir = os.fspath(root_dir) else: From 79d7fd929ab553444b3f324124d97288ae67ca0b Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Sun, 20 Apr 2025 21:15:12 +0100 Subject: [PATCH 2/7] Finish up and add tests --- Lib/glob.py | 14 +++++++++++--- Lib/test/test_glob.py | 16 ++++++++++++++++ ...025-04-20-21-12-00.gh-issue-132757.asfaeh.rst | 1 + 3 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-04-20-21-12-00.gh-issue-132757.asfaeh.rst diff --git a/Lib/glob.py b/Lib/glob.py index 61f2a0a43fd73a..9f3c4e985a7394 100644 --- a/Lib/glob.py +++ b/Lib/glob.py @@ -13,6 +13,9 @@ __all__ = ["glob", "iglob", "escape", "translate"] +from Lib import posixpath + + def glob(pathname, *, root_dir=None, dir_fd=None, recursive=False, include_hidden=False): """Return a list of paths matching a pathname pattern. @@ -46,9 +49,14 @@ def iglob(pathname, *, root_dir=None, dir_fd=None, recursive=False, sys.audit("glob.glob", pathname, recursive) sys.audit("glob.glob/2", pathname, recursive, root_dir, dir_fd) # expand ~; see issue 84037 - if pathname.startswith('~'): - from pathlib import Path - pathname = pathname.replace('~', str(Path.home()), 1) + if isinstance(pathname, str): + if pathname == '~' or pathname.startswith('~' + os.sep): + from pathlib import Path + pathname = pathname.replace('~', str(Path.home()), 1) + else: + if pathname == b'~' or pathname.startswith(b'~' + os.sep.encode('ascii')): + from pathlib import Path + pathname = pathname.replace(b'~', bytes(Path.home()), 1) if root_dir is not None: root_dir = os.fspath(root_dir) else: diff --git a/Lib/test/test_glob.py b/Lib/test/test_glob.py index 6e5fc2939c6f2c..0e37687490513b 100644 --- a/Lib/test/test_glob.py +++ b/Lib/test/test_glob.py @@ -4,6 +4,7 @@ import shutil import sys import unittest +import unittest.mock import warnings from test.support import is_wasi, Py_DEBUG @@ -210,6 +211,21 @@ def test_glob_bytes_directory_with_trailing_slash(self): [os.fsencode(self.norm('aaa') + os.sep), os.fsencode(self.norm('aab') + os.sep)]) + def test_glob_tilde_expansion(self): + with unittest.mock.patch('pathlib.Path.home', return_value=self.tempdir): + results = glob.glob('~') + self.assertEqual([self.tempdir], results) + + results = glob.glob('~/*') + self.assertIn(self.tempdir + '/a', results) + + # test it is not expanded when it is not a path + tilde_file = os.path.join(self.tempdir, '~file') + create_empty_file(tilde_file) + with change_cwd(self.tempdir): + results = glob.glob('~*') + self.assertIn('~file', results) + @skip_unless_symlink def test_glob_symlinks(self): eq = self.assertSequencesEqual_noorder diff --git a/Misc/NEWS.d/next/Library/2025-04-20-21-12-00.gh-issue-132757.asfaeh.rst b/Misc/NEWS.d/next/Library/2025-04-20-21-12-00.gh-issue-132757.asfaeh.rst new file mode 100644 index 00000000000000..f113908e65bb3a --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-04-20-21-12-00.gh-issue-132757.asfaeh.rst @@ -0,0 +1 @@ +Implement ``~`` expansion in :func:`glob.glob`. From e0de93c616b34e41629bfffb4251e472d6e86600 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Sun, 20 Apr 2025 21:17:34 +0100 Subject: [PATCH 3/7] Add versionchanged to docs --- Doc/library/glob.rst | 3 +++ Lib/glob.py | 3 --- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Doc/library/glob.rst b/Doc/library/glob.rst index 684466d354aef8..d2d80924633c96 100644 --- a/Doc/library/glob.rst +++ b/Doc/library/glob.rst @@ -88,6 +88,9 @@ The :mod:`glob` module defines the following functions: .. versionchanged:: 3.11 Added the *include_hidden* parameter. + .. versionchanged:: next + Fixed ``~`` expansion to user's home directory. + .. function:: iglob(pathname, *, root_dir=None, dir_fd=None, recursive=False, \ include_hidden=False) diff --git a/Lib/glob.py b/Lib/glob.py index 9f3c4e985a7394..db62120581f650 100644 --- a/Lib/glob.py +++ b/Lib/glob.py @@ -13,9 +13,6 @@ __all__ = ["glob", "iglob", "escape", "translate"] -from Lib import posixpath - - def glob(pathname, *, root_dir=None, dir_fd=None, recursive=False, include_hidden=False): """Return a list of paths matching a pathname pattern. From cc0e341d09e6b9cecbd03564c026036c5a1d3b8b Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Sun, 20 Apr 2025 23:30:04 +0100 Subject: [PATCH 4/7] Address review --- Doc/library/glob.rst | 2 +- Doc/whatsnew/3.14.rst | 5 +++++ Lib/glob.py | 19 ++++++++++--------- Lib/test/test_glob.py | 4 ++-- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/Doc/library/glob.rst b/Doc/library/glob.rst index d2d80924633c96..f4e77b051b0e0e 100644 --- a/Doc/library/glob.rst +++ b/Doc/library/glob.rst @@ -89,7 +89,7 @@ The :mod:`glob` module defines the following functions: Added the *include_hidden* parameter. .. versionchanged:: next - Fixed ``~`` expansion to user's home directory. + Added ``~`` expansion to user's home directory. .. function:: iglob(pathname, *, root_dir=None, dir_fd=None, recursive=False, \ diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index f02ce6bc1d4f2f..44790235653904 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -740,6 +740,11 @@ getopt * Add support for returning intermixed options and non-option arguments in order. (Contributed by Serhiy Storchaka in :gh:`126390`.) +glob +---- + +* Add ``~`` expansion to user's home directory. + (Contributed by Stan Ulbrych in :gh:`132757`.) graphlib -------- diff --git a/Lib/glob.py b/Lib/glob.py index db62120581f650..89ce75914b1605 100644 --- a/Lib/glob.py +++ b/Lib/glob.py @@ -45,15 +45,16 @@ def iglob(pathname, *, root_dir=None, dir_fd=None, recursive=False, """ sys.audit("glob.glob", pathname, recursive) sys.audit("glob.glob/2", pathname, recursive, root_dir, dir_fd) - # expand ~; see issue 84037 - if isinstance(pathname, str): - if pathname == '~' or pathname.startswith('~' + os.sep): - from pathlib import Path - pathname = pathname.replace('~', str(Path.home()), 1) - else: - if pathname == b'~' or pathname.startswith(b'~' + os.sep.encode('ascii')): - from pathlib import Path - pathname = pathname.replace(b'~', bytes(Path.home()), 1) + + # expand ~ + from pathlib import Path + tilde = '~' if isinstance(pathname, str) else b'~' + sep = os.sep if isinstance(pathname, str) else os.sep.encode('ascii') + home = str(Path.home()) if isinstance(pathname, str) else bytes( + Path.home()) + if pathname == tilde or pathname.startswith(tilde + sep): + pathname = pathname.replace(tilde, home, 1) + if root_dir is not None: root_dir = os.fspath(root_dir) else: diff --git a/Lib/test/test_glob.py b/Lib/test/test_glob.py index 0e37687490513b..6eb9f7cbdb3b72 100644 --- a/Lib/test/test_glob.py +++ b/Lib/test/test_glob.py @@ -216,8 +216,8 @@ def test_glob_tilde_expansion(self): results = glob.glob('~') self.assertEqual([self.tempdir], results) - results = glob.glob('~/*') - self.assertIn(self.tempdir + '/a', results) + results = glob.glob(f'~{os.sep}*') + self.assertIn(self.tempdir + f'{os.sep}a', results) # test it is not expanded when it is not a path tilde_file = os.path.join(self.tempdir, '~file') From d5783b3caab3c8ab1aa332fa2c6a2d587c416921 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Sun, 20 Apr 2025 23:31:42 +0100 Subject: [PATCH 5/7] Whatsnew add whitespace --- Doc/whatsnew/3.14.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 44790235653904..6f679c1e8ec9aa 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -740,12 +740,14 @@ getopt * Add support for returning intermixed options and non-option arguments in order. (Contributed by Serhiy Storchaka in :gh:`126390`.) + glob ---- * Add ``~`` expansion to user's home directory. (Contributed by Stan Ulbrych in :gh:`132757`.) + graphlib -------- From 1c8fde08878e13f73b8ad465c729ecf8e1efb724 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Mon, 21 Apr 2025 16:29:08 +0100 Subject: [PATCH 6/7] Drop pathlib and make optional --- Doc/library/glob.rst | 6 ++++-- Lib/glob.py | 26 +++++++++++++++----------- Lib/test/test_glob.py | 8 ++++---- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/Doc/library/glob.rst b/Doc/library/glob.rst index f4e77b051b0e0e..d4850445db777e 100644 --- a/Doc/library/glob.rst +++ b/Doc/library/glob.rst @@ -38,7 +38,7 @@ The :mod:`glob` module defines the following functions: .. function:: glob(pathname, *, root_dir=None, dir_fd=None, recursive=False, \ - include_hidden=False) + include_hidden=False, expand_tilde=False) Return a possibly empty list of path names that match *pathname*, which must be a string containing a path specification. *pathname* can be either absolute @@ -68,6 +68,8 @@ The :mod:`glob` module defines the following functions: If *include_hidden* is true, "``**``" pattern will match hidden directories. + If *expand_tilde* is true, ``~`` will be expanded to the users home directory. + .. audit-event:: glob.glob pathname,recursive glob.glob .. audit-event:: glob.glob/2 pathname,recursive,root_dir,dir_fd glob.glob @@ -89,7 +91,7 @@ The :mod:`glob` module defines the following functions: Added the *include_hidden* parameter. .. versionchanged:: next - Added ``~`` expansion to user's home directory. + Added the *expand_tilde* parameter. .. function:: iglob(pathname, *, root_dir=None, dir_fd=None, recursive=False, \ diff --git a/Lib/glob.py b/Lib/glob.py index 89ce75914b1605..0bf1c04c870bfb 100644 --- a/Lib/glob.py +++ b/Lib/glob.py @@ -14,7 +14,7 @@ __all__ = ["glob", "iglob", "escape", "translate"] def glob(pathname, *, root_dir=None, dir_fd=None, recursive=False, - include_hidden=False): + include_hidden=False, expand_tilde=False): """Return a list of paths matching a pathname pattern. The pattern may contain simple shell-style wildcards a la @@ -29,10 +29,10 @@ def glob(pathname, *, root_dir=None, dir_fd=None, recursive=False, zero or more directories and subdirectories. """ return list(iglob(pathname, root_dir=root_dir, dir_fd=dir_fd, recursive=recursive, - include_hidden=include_hidden)) + include_hidden=include_hidden, expand_tilde=expand_tilde)) def iglob(pathname, *, root_dir=None, dir_fd=None, recursive=False, - include_hidden=False): + include_hidden=False, expand_tilde=False): """Return an iterator which yields the paths matching a pathname pattern. The pattern may contain simple shell-style wildcards a la @@ -46,14 +46,12 @@ def iglob(pathname, *, root_dir=None, dir_fd=None, recursive=False, sys.audit("glob.glob", pathname, recursive) sys.audit("glob.glob/2", pathname, recursive, root_dir, dir_fd) - # expand ~ - from pathlib import Path - tilde = '~' if isinstance(pathname, str) else b'~' - sep = os.sep if isinstance(pathname, str) else os.sep.encode('ascii') - home = str(Path.home()) if isinstance(pathname, str) else bytes( - Path.home()) - if pathname == tilde or pathname.startswith(tilde + sep): - pathname = pathname.replace(tilde, home, 1) + if expand_tilde: + tilde = '~' if isinstance(pathname, str) else b'~' + sep = os.path.sep if isinstance(pathname, str) else os.path.sep.encode('ascii') + home = str(os.path.expanduser('~')) if isinstance(pathname, str) else os.path.expanduser('~').encode('ascii') + if pathname == tilde or pathname.startswith(tilde + sep): + pathname = pathname.replace(tilde, home, 1) if root_dir is not None: root_dir = os.fspath(root_dir) @@ -555,3 +553,9 @@ def scandir(path): @staticmethod def concat_path(path, text): return path.with_segments(str(path) + text) + +if __name__ == '__main__': + from pathlib import Path + print('os:', os.path.expanduser('~')) + print('pathlib:', Path.home()) + assert str(os.path.expanduser('~')) == str(Path.home()) diff --git a/Lib/test/test_glob.py b/Lib/test/test_glob.py index 6eb9f7cbdb3b72..a4551901d48b8d 100644 --- a/Lib/test/test_glob.py +++ b/Lib/test/test_glob.py @@ -212,18 +212,18 @@ def test_glob_bytes_directory_with_trailing_slash(self): os.fsencode(self.norm('aab') + os.sep)]) def test_glob_tilde_expansion(self): - with unittest.mock.patch('pathlib.Path.home', return_value=self.tempdir): - results = glob.glob('~') + with unittest.mock.patch('os.path.expanduser', return_value=self.tempdir): + results = glob.glob('~', expand_tilde=True) self.assertEqual([self.tempdir], results) - results = glob.glob(f'~{os.sep}*') + results = glob.glob(f'~{os.sep}*', expand_tilde=True) self.assertIn(self.tempdir + f'{os.sep}a', results) # test it is not expanded when it is not a path tilde_file = os.path.join(self.tempdir, '~file') create_empty_file(tilde_file) with change_cwd(self.tempdir): - results = glob.glob('~*') + results = glob.glob('~*', expand_tilde=True) self.assertIn('~file', results) @skip_unless_symlink From a338d4fc393d1ca7c75fab2359618833524edc32 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Mon, 21 Apr 2025 16:34:31 +0100 Subject: [PATCH 7/7] Clean up --- Doc/whatsnew/3.14.rst | 2 +- Lib/glob.py | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 6f679c1e8ec9aa..91f1a0c4dd5825 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -744,7 +744,7 @@ getopt glob ---- -* Add ``~`` expansion to user's home directory. +* Add *expand_tilde* option to :func:`~glob.glob`. (Contributed by Stan Ulbrych in :gh:`132757`.) diff --git a/Lib/glob.py b/Lib/glob.py index 0bf1c04c870bfb..0b1758a7d2a89a 100644 --- a/Lib/glob.py +++ b/Lib/glob.py @@ -553,9 +553,3 @@ def scandir(path): @staticmethod def concat_path(path, text): return path.with_segments(str(path) + text) - -if __name__ == '__main__': - from pathlib import Path - print('os:', os.path.expanduser('~')) - print('pathlib:', Path.home()) - assert str(os.path.expanduser('~')) == str(Path.home())