From 2c08523395fd17b7c79c4b4f08f699c96c18d001 Mon Sep 17 00:00:00 2001 From: barneygale Date: Mon, 18 Dec 2023 08:29:01 +0000 Subject: [PATCH 1/2] GH-65238: Add test cases for trailing slash handling in pathlib Ensure that trailing slashes are ignored whenever pathlib splits a basename from a dirname. This commit adds test cases for `parent`, `parents`, `name`, `stem`, `suffix`, `suffixes`, `with_name()`, `with_stem()`, `with_suffix()`, `relative_to()`, `is_relative_to()`, `expanduser()` and `absolute()`. Any solution for GH-65238 should keep these tests passing. --- Lib/test/test_pathlib/test_pathlib.py | 57 ++++++++++++++++ Lib/test/test_pathlib/test_pathlib_abc.py | 80 +++++++++++++++++++++++ 2 files changed, 137 insertions(+) diff --git a/Lib/test/test_pathlib/test_pathlib.py b/Lib/test/test_pathlib/test_pathlib.py index 00cfdd37e568a6..e620dfb550159d 100644 --- a/Lib/test/test_pathlib/test_pathlib.py +++ b/Lib/test/test_pathlib/test_pathlib.py @@ -439,10 +439,30 @@ def test_parent(self): self.assertEqual(p.parent, P('//a/b/c')) self.assertEqual(p.parent.parent, P('//a/b')) self.assertEqual(p.parent.parent.parent, P('//a/b')) + # Trailing sep + p = P('z:/a/b/c/') + self.assertEqual(p.parent, P('z:/a/b')) + self.assertEqual(p.parent.parent, P('z:/a')) + self.assertEqual(p.parent.parent.parent, P('z:/')) + self.assertEqual(p.parent.parent.parent.parent, P('z:/')) def test_parents(self): # Anchored P = self.cls + p = P('z:a/b') + par = p.parents + self.assertEqual(len(par), 2) + self.assertEqual(par[0], P('z:a')) + self.assertEqual(par[1], P('z:')) + self.assertEqual(par[0:1], (P('z:a'),)) + self.assertEqual(par[:-1], (P('z:a'),)) + self.assertEqual(par[:2], (P('z:a'), P('z:'))) + self.assertEqual(par[1:], (P('z:'),)) + self.assertEqual(par[::2], (P('z:a'),)) + self.assertEqual(par[::-1], (P('z:'), P('z:a'))) + self.assertEqual(list(par), [P('z:a'), P('z:')]) + with self.assertRaises(IndexError): + par[2] p = P('z:a/b/') par = p.parents self.assertEqual(len(par), 2) @@ -455,6 +475,20 @@ def test_parents(self): self.assertEqual(par[::2], (P('z:a'),)) self.assertEqual(par[::-1], (P('z:'), P('z:a'))) self.assertEqual(list(par), [P('z:a'), P('z:')]) + with self.assertRaises(IndexError): + par[2] + p = P('z:/a/b') + par = p.parents + self.assertEqual(len(par), 2) + self.assertEqual(par[0], P('z:/a')) + self.assertEqual(par[1], P('z:/')) + self.assertEqual(par[0:1], (P('z:/a'),)) + self.assertEqual(par[0:-1], (P('z:/a'),)) + self.assertEqual(par[:2], (P('z:/a'), P('z:/'))) + self.assertEqual(par[1:], (P('z:/'),)) + self.assertEqual(par[::2], (P('z:/a'),)) + self.assertEqual(par[::-1], (P('z:/'), P('z:/a'),)) + self.assertEqual(list(par), [P('z:/a'), P('z:/')]) with self.assertRaises(IndexError): par[2] p = P('z:/a/b/') @@ -522,18 +556,23 @@ def test_name(self): self.assertEqual(P('c:').name, '') self.assertEqual(P('c:/').name, '') self.assertEqual(P('c:a/b').name, 'b') + self.assertEqual(P('c:a/b/').name, 'b') self.assertEqual(P('c:/a/b').name, 'b') + self.assertEqual(P('c:/a/b/').name, 'b') self.assertEqual(P('c:a/b.py').name, 'b.py') self.assertEqual(P('c:/a/b.py').name, 'b.py') self.assertEqual(P('//My.py/Share.php').name, '') self.assertEqual(P('//My.py/Share.php/a/b').name, 'b') + self.assertEqual(P('c:/etc/cron.d/').name, 'cron.d') def test_suffix(self): P = self.cls self.assertEqual(P('c:').suffix, '') self.assertEqual(P('c:/').suffix, '') self.assertEqual(P('c:a/b').suffix, '') + self.assertEqual(P('c:a/b/').suffix, '') self.assertEqual(P('c:/a/b').suffix, '') + self.assertEqual(P('c:/a/b/').suffix, '') self.assertEqual(P('c:a/b.py').suffix, '.py') self.assertEqual(P('c:/a/b.py').suffix, '.py') self.assertEqual(P('c:a/.hgrc').suffix, '') @@ -546,13 +585,16 @@ def test_suffix(self): self.assertEqual(P('c:/a/Some name. Ending with a dot.').suffix, '') self.assertEqual(P('//My.py/Share.php').suffix, '') self.assertEqual(P('//My.py/Share.php/a/b').suffix, '') + self.assertEqual(P('c:/etc/cron.d/').suffix, '.d') def test_suffixes(self): P = self.cls self.assertEqual(P('c:').suffixes, []) self.assertEqual(P('c:/').suffixes, []) self.assertEqual(P('c:a/b').suffixes, []) + self.assertEqual(P('c:a/b/').suffixes, []) self.assertEqual(P('c:/a/b').suffixes, []) + self.assertEqual(P('c:/a/b/').suffixes, []) self.assertEqual(P('c:a/b.py').suffixes, ['.py']) self.assertEqual(P('c:/a/b.py').suffixes, ['.py']) self.assertEqual(P('c:a/.hgrc').suffixes, []) @@ -565,6 +607,7 @@ def test_suffixes(self): self.assertEqual(P('//My.py/Share.php/a/b').suffixes, []) self.assertEqual(P('c:a/Some name. Ending with a dot.').suffixes, []) self.assertEqual(P('c:/a/Some name. Ending with a dot.').suffixes, []) + self.assertEqual(P('c:/etc/cron.d/').suffixes, ['.d']) def test_stem(self): P = self.cls @@ -573,12 +616,14 @@ def test_stem(self): self.assertEqual(P('c:..').stem, '..') self.assertEqual(P('c:/').stem, '') self.assertEqual(P('c:a/b').stem, 'b') + self.assertEqual(P('c:a/b/').stem, 'b') self.assertEqual(P('c:a/b.py').stem, 'b') self.assertEqual(P('c:a/.hgrc').stem, '.hgrc') self.assertEqual(P('c:a/.hg.rc').stem, '.hg') self.assertEqual(P('c:a/b.tar.gz').stem, 'b.tar') self.assertEqual(P('c:a/Some name. Ending with a dot.').stem, 'Some name. Ending with a dot.') + self.assertEqual(P('c:/etc/cron.d/').stem, 'cron') def test_with_name(self): P = self.cls @@ -586,6 +631,7 @@ def test_with_name(self): self.assertEqual(P('c:/a/b').with_name('d.xml'), P('c:/a/d.xml')) self.assertEqual(P('c:a/Dot ending.').with_name('d.xml'), P('c:a/d.xml')) self.assertEqual(P('c:/a/Dot ending.').with_name('d.xml'), P('c:/a/d.xml')) + self.assertEqual(P('c:/etc/cron.d/').with_name('tron.g'), P('c:/etc/tron.g/')) self.assertRaises(ValueError, P('c:').with_name, 'd.xml') self.assertRaises(ValueError, P('c:/').with_name, 'd.xml') self.assertRaises(ValueError, P('//My/Share').with_name, 'd.xml') @@ -602,6 +648,7 @@ def test_with_stem(self): self.assertEqual(P('c:/a/b').with_stem('d'), P('c:/a/d')) self.assertEqual(P('c:a/Dot ending.').with_stem('d'), P('c:a/d')) self.assertEqual(P('c:/a/Dot ending.').with_stem('d'), P('c:/a/d')) + self.assertEqual(P('c:/etc/cron.d/').with_stem('tron'), P('c:/etc/tron.d/')) self.assertRaises(ValueError, P('c:').with_stem, 'd') self.assertRaises(ValueError, P('c:/').with_stem, 'd') self.assertRaises(ValueError, P('//My/Share').with_stem, 'd') @@ -618,6 +665,7 @@ def test_with_suffix(self): self.assertEqual(P('c:/a/b').with_suffix('.gz'), P('c:/a/b.gz')) self.assertEqual(P('c:a/b.py').with_suffix('.gz'), P('c:a/b.gz')) self.assertEqual(P('c:/a/b.py').with_suffix('.gz'), P('c:/a/b.gz')) + self.assertEqual(P('c:/etc/cron.d/').with_suffix('.g'), P('c:/etc/cron.g/')) # Path doesn't have a "filename" component. self.assertRaises(ValueError, P('').with_suffix, '.gz') self.assertRaises(ValueError, P('.').with_suffix, '.gz') @@ -1018,6 +1066,12 @@ def test_expanduser_common(self): P = self.cls p = P('~') self.assertEqual(p.expanduser(), P(os.path.expanduser('~'))) + p = P('~/') + self.assertEqual(p.expanduser(), P(os.path.expanduser('~/'))) + p = P('~/foo') + self.assertEqual(p.expanduser(), P(os.path.expanduser('~/foo'))) + p = P('~/foo/') + self.assertEqual(p.expanduser(), P(os.path.expanduser('~/foo/'))) p = P('foo') self.assertEqual(p.expanduser(), p) p = P('/~') @@ -1797,10 +1851,12 @@ def test_absolute(self): # Relative path with root self.assertEqual(str(P('\\').absolute()), drive + '\\') self.assertEqual(str(P('\\foo').absolute()), drive + '\\foo') + self.assertEqual(str(P('\\foo\\').absolute()), drive + '\\foo\\') # Relative path on current drive self.assertEqual(str(P(drive).absolute()), self.base) self.assertEqual(str(P(drive + 'foo').absolute()), os.path.join(self.base, 'foo')) + self.assertEqual(str(P(drive + 'foo\\').absolute()), os.path.join(self.base, 'foo\\')) with os_helper.subst_drive(self.base) as other_drive: # Set the working directory on the substitute drive @@ -1812,6 +1868,7 @@ def test_absolute(self): # Relative path on another drive self.assertEqual(str(P(other_drive).absolute()), other_cwd) self.assertEqual(str(P(other_drive + 'foo').absolute()), other_cwd + '\\foo') + self.assertEqual(str(P(other_drive + 'foo\\').absolute()), other_cwd + '\\foo\\') def test_glob(self): P = self.cls diff --git a/Lib/test/test_pathlib/test_pathlib_abc.py b/Lib/test/test_pathlib/test_pathlib_abc.py index a272973d9c1d61..f7f0a8d72b1c70 100644 --- a/Lib/test/test_pathlib/test_pathlib_abc.py +++ b/Lib/test/test_pathlib/test_pathlib_abc.py @@ -365,6 +365,12 @@ def test_parent_common(self): self.assertEqual(p.parent.parent, P('/a')) self.assertEqual(p.parent.parent.parent, P('/')) self.assertEqual(p.parent.parent.parent.parent, P('/')) + # Trailing sep + p = P('/a/b/c/') + self.assertEqual(p.parent, P('/a/b')) + self.assertEqual(p.parent.parent, P('/a')) + self.assertEqual(p.parent.parent.parent, P('/')) + self.assertEqual(p.parent.parent.parent.parent, P('/')) def test_parents_common(self): # Relative @@ -412,6 +418,27 @@ def test_parents_common(self): par[-4] with self.assertRaises(IndexError): par[3] + # Trailing sep + p = P('/a/b/c/') + par = p.parents + self.assertEqual(len(par), 3) + self.assertEqual(par[0], P('/a/b')) + self.assertEqual(par[1], P('/a')) + self.assertEqual(par[2], P('/')) + self.assertEqual(par[-1], P('/')) + self.assertEqual(par[-2], P('/a')) + self.assertEqual(par[-3], P('/a/b')) + self.assertEqual(par[0:1], (P('/a/b'),)) + self.assertEqual(par[:2], (P('/a/b'), P('/a'))) + self.assertEqual(par[:-1], (P('/a/b'), P('/a'))) + self.assertEqual(par[1:], (P('/a'), P('/'))) + self.assertEqual(par[::2], (P('/a/b'), P('/'))) + self.assertEqual(par[::-1], (P('/'), P('/a'), P('/a/b'))) + self.assertEqual(list(par), [P('/a/b'), P('/a'), P('/')]) + with self.assertRaises(IndexError): + par[-4] + with self.assertRaises(IndexError): + par[3] def test_drive_common(self): P = self.cls @@ -441,10 +468,13 @@ def test_name_common(self): self.assertEqual(P('.').name, '') self.assertEqual(P('/').name, '') self.assertEqual(P('a/b').name, 'b') + self.assertEqual(P('a/b/').name, 'b') self.assertEqual(P('/a/b').name, 'b') + self.assertEqual(P('/a/b/').name, 'b') self.assertEqual(P('/a/b/.').name, 'b') self.assertEqual(P('a/b.py').name, 'b.py') self.assertEqual(P('/a/b.py').name, 'b.py') + self.assertEqual(P('/etc/cron.d/').name, 'cron.d') def test_suffix_common(self): P = self.cls @@ -453,7 +483,9 @@ def test_suffix_common(self): self.assertEqual(P('..').suffix, '') self.assertEqual(P('/').suffix, '') self.assertEqual(P('a/b').suffix, '') + self.assertEqual(P('a/b/').suffix, '') self.assertEqual(P('/a/b').suffix, '') + self.assertEqual(P('/a/b/').suffix, '') self.assertEqual(P('/a/b/.').suffix, '') self.assertEqual(P('a/b.py').suffix, '.py') self.assertEqual(P('/a/b.py').suffix, '.py') @@ -465,6 +497,7 @@ def test_suffix_common(self): self.assertEqual(P('/a/b.tar.gz').suffix, '.gz') self.assertEqual(P('a/Some name. Ending with a dot.').suffix, '') self.assertEqual(P('/a/Some name. Ending with a dot.').suffix, '') + self.assertEqual(P('/etc/cron.d/').suffix, '.d') def test_suffixes_common(self): P = self.cls @@ -472,7 +505,9 @@ def test_suffixes_common(self): self.assertEqual(P('.').suffixes, []) self.assertEqual(P('/').suffixes, []) self.assertEqual(P('a/b').suffixes, []) + self.assertEqual(P('a/b/').suffixes, []) self.assertEqual(P('/a/b').suffixes, []) + self.assertEqual(P('/a/b/').suffixes, []) self.assertEqual(P('/a/b/.').suffixes, []) self.assertEqual(P('a/b.py').suffixes, ['.py']) self.assertEqual(P('/a/b.py').suffixes, ['.py']) @@ -484,6 +519,7 @@ def test_suffixes_common(self): self.assertEqual(P('/a/b.tar.gz').suffixes, ['.tar', '.gz']) self.assertEqual(P('a/Some name. Ending with a dot.').suffixes, []) self.assertEqual(P('/a/Some name. Ending with a dot.').suffixes, []) + self.assertEqual(P('/etc/cron.d/').suffixes, ['.d']) def test_stem_common(self): P = self.cls @@ -492,12 +528,14 @@ def test_stem_common(self): self.assertEqual(P('..').stem, '..') self.assertEqual(P('/').stem, '') self.assertEqual(P('a/b').stem, 'b') + self.assertEqual(P('a/b/').stem, 'b') self.assertEqual(P('a/b.py').stem, 'b') self.assertEqual(P('a/.hgrc').stem, '.hgrc') self.assertEqual(P('a/.hg.rc').stem, '.hg') self.assertEqual(P('a/b.tar.gz').stem, 'b.tar') self.assertEqual(P('a/Some name. Ending with a dot.').stem, 'Some name. Ending with a dot.') + self.assertEqual(P('/etc/cron.d/').stem, 'cron') def test_with_name_common(self): P = self.cls @@ -507,6 +545,7 @@ def test_with_name_common(self): self.assertEqual(P('/a/b.py').with_name('d.xml'), P('/a/d.xml')) self.assertEqual(P('a/Dot ending.').with_name('d.xml'), P('a/d.xml')) self.assertEqual(P('/a/Dot ending.').with_name('d.xml'), P('/a/d.xml')) + self.assertEqual(P('/etc/cron.d/').with_name('tron.g'), P('/etc/tron.g/')) self.assertRaises(ValueError, P('').with_name, 'd.xml') self.assertRaises(ValueError, P('.').with_name, 'd.xml') self.assertRaises(ValueError, P('/').with_name, 'd.xml') @@ -525,6 +564,7 @@ def test_with_stem_common(self): self.assertEqual(P('/a/b.tar.gz').with_stem('d'), P('/a/d.gz')) self.assertEqual(P('a/Dot ending.').with_stem('d'), P('a/d')) self.assertEqual(P('/a/Dot ending.').with_stem('d'), P('/a/d')) + self.assertEqual(P('/etc/cron.d/').with_stem('tron'), P('/etc/tron.d/')) self.assertRaises(ValueError, P('').with_stem, 'd') self.assertRaises(ValueError, P('.').with_stem, 'd') self.assertRaises(ValueError, P('/').with_stem, 'd') @@ -540,9 +580,11 @@ def test_with_suffix_common(self): self.assertEqual(P('/a/b').with_suffix('.gz'), P('/a/b.gz')) self.assertEqual(P('a/b.py').with_suffix('.gz'), P('a/b.gz')) self.assertEqual(P('/a/b.py').with_suffix('.gz'), P('/a/b.gz')) + self.assertEqual(P('/etc/cron.d/').with_suffix('.g'), P('/etc/cron.g/')) # Stripping suffix. self.assertEqual(P('a/b.py').with_suffix(''), P('a/b')) self.assertEqual(P('/a/b').with_suffix(''), P('/a/b')) + self.assertEqual(P('/etc/cron.d/').with_suffix(''), P('/etc/cron/')) # Path doesn't have a "filename" component. self.assertRaises(ValueError, P('').with_suffix, '.gz') self.assertRaises(ValueError, P('.').with_suffix, '.gz') @@ -636,6 +678,25 @@ def test_relative_to_common(self): self.assertRaises(ValueError, p.relative_to, P("a/.."), walk_up=True) self.assertRaises(ValueError, p.relative_to, P("/a/.."), walk_up=True) + def test_relative_to_trailing_sep(self): + P = self.cls + self.assertEqual(P('foo').relative_to('foo'), P()) + self.assertEqual(P('foo').relative_to('foo/'), P()) + self.assertEqual(P('foo/').relative_to('foo'), P()) + self.assertEqual(P('foo/').relative_to('foo/'), P()) + self.assertEqual(P('foo/bar').relative_to('foo'), P('bar')) + self.assertEqual(P('foo/bar').relative_to('foo/'), P('bar')) + self.assertEqual(P('foo/bar/').relative_to('foo'), P('bar/')) + self.assertEqual(P('foo/bar/').relative_to('foo/'), P('bar/')) + self.assertEqual(P('foo').relative_to('foo/bar', walk_up=True), P('..')) + self.assertEqual(P('foo').relative_to('foo/bar/', walk_up=True), P('..')) + self.assertEqual(P('foo/').relative_to('foo/bar', walk_up=True), P('../')) + self.assertEqual(P('foo/').relative_to('foo/bar/', walk_up=True), P('../')) + self.assertEqual(P('foo/oof').relative_to('foo/bar', walk_up=True), P('../oof')) + self.assertEqual(P('foo/oof').relative_to('foo/bar/', walk_up=True), P('../oof')) + self.assertEqual(P('foo/oof/').relative_to('foo/bar', walk_up=True), P('../oof/')) + self.assertEqual(P('foo/oof/').relative_to('foo/bar/', walk_up=True), P('../oof/')) + def test_is_relative_to_common(self): P = self.cls p = P('a/b') @@ -671,6 +732,25 @@ def test_is_relative_to_common(self): self.assertFalse(p.is_relative_to('')) self.assertFalse(p.is_relative_to(P('a'))) + def test_is_relative_to_trailing_sep(self): + P = self.cls + self.assertTrue(P('foo').is_relative_to('foo')) + self.assertTrue(P('foo').is_relative_to('foo/')) + self.assertTrue(P('foo/').is_relative_to('foo')) + self.assertTrue(P('foo/').is_relative_to('foo/')) + self.assertTrue(P('foo/bar').is_relative_to('foo')) + self.assertTrue(P('foo/bar').is_relative_to('foo/')) + self.assertTrue(P('foo/bar/').is_relative_to('foo')) + self.assertTrue(P('foo/bar/').is_relative_to('foo/')) + self.assertFalse(P('foo').is_relative_to('foo/bar')) + self.assertFalse(P('foo').is_relative_to('foo/bar/')) + self.assertFalse(P('foo/').is_relative_to('foo/bar')) + self.assertFalse(P('foo/').is_relative_to('foo/bar/')) + self.assertFalse(P('foo/oof').is_relative_to('foo/bar')) + self.assertFalse(P('foo/oof').is_relative_to('foo/bar/')) + self.assertFalse(P('foo/oof/').is_relative_to('foo/bar')) + self.assertFalse(P('foo/oof/').is_relative_to('foo/bar/')) + # # Tests for the virtual classes. From a9a8f0c4e31c9b05ec7abb5a7216fd08bb59b89f Mon Sep 17 00:00:00 2001 From: barneygale Date: Mon, 18 Dec 2023 09:01:24 +0000 Subject: [PATCH 2/2] Undo `WindowsPath.test_absolute()` additions. --- Lib/test/test_pathlib/test_pathlib.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/Lib/test/test_pathlib/test_pathlib.py b/Lib/test/test_pathlib/test_pathlib.py index e620dfb550159d..41a8a438897e3b 100644 --- a/Lib/test/test_pathlib/test_pathlib.py +++ b/Lib/test/test_pathlib/test_pathlib.py @@ -1851,12 +1851,10 @@ def test_absolute(self): # Relative path with root self.assertEqual(str(P('\\').absolute()), drive + '\\') self.assertEqual(str(P('\\foo').absolute()), drive + '\\foo') - self.assertEqual(str(P('\\foo\\').absolute()), drive + '\\foo\\') # Relative path on current drive self.assertEqual(str(P(drive).absolute()), self.base) self.assertEqual(str(P(drive + 'foo').absolute()), os.path.join(self.base, 'foo')) - self.assertEqual(str(P(drive + 'foo\\').absolute()), os.path.join(self.base, 'foo\\')) with os_helper.subst_drive(self.base) as other_drive: # Set the working directory on the substitute drive @@ -1868,7 +1866,6 @@ def test_absolute(self): # Relative path on another drive self.assertEqual(str(P(other_drive).absolute()), other_cwd) self.assertEqual(str(P(other_drive + 'foo').absolute()), other_cwd + '\\foo') - self.assertEqual(str(P(other_drive + 'foo\\').absolute()), other_cwd + '\\foo\\') def test_glob(self): P = self.cls