Skip to content

Commit 2c08523

Browse files
committed
pythonGH-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 pythonGH-65238 should keep these tests passing.
1 parent 2f0ec7f commit 2c08523

File tree

2 files changed

+137
-0
lines changed

2 files changed

+137
-0
lines changed

Lib/test/test_pathlib/test_pathlib.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,10 +439,30 @@ def test_parent(self):
439439
self.assertEqual(p.parent, P('//a/b/c'))
440440
self.assertEqual(p.parent.parent, P('//a/b'))
441441
self.assertEqual(p.parent.parent.parent, P('//a/b'))
442+
# Trailing sep
443+
p = P('z:/a/b/c/')
444+
self.assertEqual(p.parent, P('z:/a/b'))
445+
self.assertEqual(p.parent.parent, P('z:/a'))
446+
self.assertEqual(p.parent.parent.parent, P('z:/'))
447+
self.assertEqual(p.parent.parent.parent.parent, P('z:/'))
442448

443449
def test_parents(self):
444450
# Anchored
445451
P = self.cls
452+
p = P('z:a/b')
453+
par = p.parents
454+
self.assertEqual(len(par), 2)
455+
self.assertEqual(par[0], P('z:a'))
456+
self.assertEqual(par[1], P('z:'))
457+
self.assertEqual(par[0:1], (P('z:a'),))
458+
self.assertEqual(par[:-1], (P('z:a'),))
459+
self.assertEqual(par[:2], (P('z:a'), P('z:')))
460+
self.assertEqual(par[1:], (P('z:'),))
461+
self.assertEqual(par[::2], (P('z:a'),))
462+
self.assertEqual(par[::-1], (P('z:'), P('z:a')))
463+
self.assertEqual(list(par), [P('z:a'), P('z:')])
464+
with self.assertRaises(IndexError):
465+
par[2]
446466
p = P('z:a/b/')
447467
par = p.parents
448468
self.assertEqual(len(par), 2)
@@ -455,6 +475,20 @@ def test_parents(self):
455475
self.assertEqual(par[::2], (P('z:a'),))
456476
self.assertEqual(par[::-1], (P('z:'), P('z:a')))
457477
self.assertEqual(list(par), [P('z:a'), P('z:')])
478+
with self.assertRaises(IndexError):
479+
par[2]
480+
p = P('z:/a/b')
481+
par = p.parents
482+
self.assertEqual(len(par), 2)
483+
self.assertEqual(par[0], P('z:/a'))
484+
self.assertEqual(par[1], P('z:/'))
485+
self.assertEqual(par[0:1], (P('z:/a'),))
486+
self.assertEqual(par[0:-1], (P('z:/a'),))
487+
self.assertEqual(par[:2], (P('z:/a'), P('z:/')))
488+
self.assertEqual(par[1:], (P('z:/'),))
489+
self.assertEqual(par[::2], (P('z:/a'),))
490+
self.assertEqual(par[::-1], (P('z:/'), P('z:/a'),))
491+
self.assertEqual(list(par), [P('z:/a'), P('z:/')])
458492
with self.assertRaises(IndexError):
459493
par[2]
460494
p = P('z:/a/b/')
@@ -522,18 +556,23 @@ def test_name(self):
522556
self.assertEqual(P('c:').name, '')
523557
self.assertEqual(P('c:/').name, '')
524558
self.assertEqual(P('c:a/b').name, 'b')
559+
self.assertEqual(P('c:a/b/').name, 'b')
525560
self.assertEqual(P('c:/a/b').name, 'b')
561+
self.assertEqual(P('c:/a/b/').name, 'b')
526562
self.assertEqual(P('c:a/b.py').name, 'b.py')
527563
self.assertEqual(P('c:/a/b.py').name, 'b.py')
528564
self.assertEqual(P('//My.py/Share.php').name, '')
529565
self.assertEqual(P('//My.py/Share.php/a/b').name, 'b')
566+
self.assertEqual(P('c:/etc/cron.d/').name, 'cron.d')
530567

531568
def test_suffix(self):
532569
P = self.cls
533570
self.assertEqual(P('c:').suffix, '')
534571
self.assertEqual(P('c:/').suffix, '')
535572
self.assertEqual(P('c:a/b').suffix, '')
573+
self.assertEqual(P('c:a/b/').suffix, '')
536574
self.assertEqual(P('c:/a/b').suffix, '')
575+
self.assertEqual(P('c:/a/b/').suffix, '')
537576
self.assertEqual(P('c:a/b.py').suffix, '.py')
538577
self.assertEqual(P('c:/a/b.py').suffix, '.py')
539578
self.assertEqual(P('c:a/.hgrc').suffix, '')
@@ -546,13 +585,16 @@ def test_suffix(self):
546585
self.assertEqual(P('c:/a/Some name. Ending with a dot.').suffix, '')
547586
self.assertEqual(P('//My.py/Share.php').suffix, '')
548587
self.assertEqual(P('//My.py/Share.php/a/b').suffix, '')
588+
self.assertEqual(P('c:/etc/cron.d/').suffix, '.d')
549589

550590
def test_suffixes(self):
551591
P = self.cls
552592
self.assertEqual(P('c:').suffixes, [])
553593
self.assertEqual(P('c:/').suffixes, [])
554594
self.assertEqual(P('c:a/b').suffixes, [])
595+
self.assertEqual(P('c:a/b/').suffixes, [])
555596
self.assertEqual(P('c:/a/b').suffixes, [])
597+
self.assertEqual(P('c:/a/b/').suffixes, [])
556598
self.assertEqual(P('c:a/b.py').suffixes, ['.py'])
557599
self.assertEqual(P('c:/a/b.py').suffixes, ['.py'])
558600
self.assertEqual(P('c:a/.hgrc').suffixes, [])
@@ -565,6 +607,7 @@ def test_suffixes(self):
565607
self.assertEqual(P('//My.py/Share.php/a/b').suffixes, [])
566608
self.assertEqual(P('c:a/Some name. Ending with a dot.').suffixes, [])
567609
self.assertEqual(P('c:/a/Some name. Ending with a dot.').suffixes, [])
610+
self.assertEqual(P('c:/etc/cron.d/').suffixes, ['.d'])
568611

569612
def test_stem(self):
570613
P = self.cls
@@ -573,19 +616,22 @@ def test_stem(self):
573616
self.assertEqual(P('c:..').stem, '..')
574617
self.assertEqual(P('c:/').stem, '')
575618
self.assertEqual(P('c:a/b').stem, 'b')
619+
self.assertEqual(P('c:a/b/').stem, 'b')
576620
self.assertEqual(P('c:a/b.py').stem, 'b')
577621
self.assertEqual(P('c:a/.hgrc').stem, '.hgrc')
578622
self.assertEqual(P('c:a/.hg.rc').stem, '.hg')
579623
self.assertEqual(P('c:a/b.tar.gz').stem, 'b.tar')
580624
self.assertEqual(P('c:a/Some name. Ending with a dot.').stem,
581625
'Some name. Ending with a dot.')
626+
self.assertEqual(P('c:/etc/cron.d/').stem, 'cron')
582627

583628
def test_with_name(self):
584629
P = self.cls
585630
self.assertEqual(P('c:a/b').with_name('d.xml'), P('c:a/d.xml'))
586631
self.assertEqual(P('c:/a/b').with_name('d.xml'), P('c:/a/d.xml'))
587632
self.assertEqual(P('c:a/Dot ending.').with_name('d.xml'), P('c:a/d.xml'))
588633
self.assertEqual(P('c:/a/Dot ending.').with_name('d.xml'), P('c:/a/d.xml'))
634+
self.assertEqual(P('c:/etc/cron.d/').with_name('tron.g'), P('c:/etc/tron.g/'))
589635
self.assertRaises(ValueError, P('c:').with_name, 'd.xml')
590636
self.assertRaises(ValueError, P('c:/').with_name, 'd.xml')
591637
self.assertRaises(ValueError, P('//My/Share').with_name, 'd.xml')
@@ -602,6 +648,7 @@ def test_with_stem(self):
602648
self.assertEqual(P('c:/a/b').with_stem('d'), P('c:/a/d'))
603649
self.assertEqual(P('c:a/Dot ending.').with_stem('d'), P('c:a/d'))
604650
self.assertEqual(P('c:/a/Dot ending.').with_stem('d'), P('c:/a/d'))
651+
self.assertEqual(P('c:/etc/cron.d/').with_stem('tron'), P('c:/etc/tron.d/'))
605652
self.assertRaises(ValueError, P('c:').with_stem, 'd')
606653
self.assertRaises(ValueError, P('c:/').with_stem, 'd')
607654
self.assertRaises(ValueError, P('//My/Share').with_stem, 'd')
@@ -618,6 +665,7 @@ def test_with_suffix(self):
618665
self.assertEqual(P('c:/a/b').with_suffix('.gz'), P('c:/a/b.gz'))
619666
self.assertEqual(P('c:a/b.py').with_suffix('.gz'), P('c:a/b.gz'))
620667
self.assertEqual(P('c:/a/b.py').with_suffix('.gz'), P('c:/a/b.gz'))
668+
self.assertEqual(P('c:/etc/cron.d/').with_suffix('.g'), P('c:/etc/cron.g/'))
621669
# Path doesn't have a "filename" component.
622670
self.assertRaises(ValueError, P('').with_suffix, '.gz')
623671
self.assertRaises(ValueError, P('.').with_suffix, '.gz')
@@ -1018,6 +1066,12 @@ def test_expanduser_common(self):
10181066
P = self.cls
10191067
p = P('~')
10201068
self.assertEqual(p.expanduser(), P(os.path.expanduser('~')))
1069+
p = P('~/')
1070+
self.assertEqual(p.expanduser(), P(os.path.expanduser('~/')))
1071+
p = P('~/foo')
1072+
self.assertEqual(p.expanduser(), P(os.path.expanduser('~/foo')))
1073+
p = P('~/foo/')
1074+
self.assertEqual(p.expanduser(), P(os.path.expanduser('~/foo/')))
10211075
p = P('foo')
10221076
self.assertEqual(p.expanduser(), p)
10231077
p = P('/~')
@@ -1797,10 +1851,12 @@ def test_absolute(self):
17971851
# Relative path with root
17981852
self.assertEqual(str(P('\\').absolute()), drive + '\\')
17991853
self.assertEqual(str(P('\\foo').absolute()), drive + '\\foo')
1854+
self.assertEqual(str(P('\\foo\\').absolute()), drive + '\\foo\\')
18001855

18011856
# Relative path on current drive
18021857
self.assertEqual(str(P(drive).absolute()), self.base)
18031858
self.assertEqual(str(P(drive + 'foo').absolute()), os.path.join(self.base, 'foo'))
1859+
self.assertEqual(str(P(drive + 'foo\\').absolute()), os.path.join(self.base, 'foo\\'))
18041860

18051861
with os_helper.subst_drive(self.base) as other_drive:
18061862
# Set the working directory on the substitute drive
@@ -1812,6 +1868,7 @@ def test_absolute(self):
18121868
# Relative path on another drive
18131869
self.assertEqual(str(P(other_drive).absolute()), other_cwd)
18141870
self.assertEqual(str(P(other_drive + 'foo').absolute()), other_cwd + '\\foo')
1871+
self.assertEqual(str(P(other_drive + 'foo\\').absolute()), other_cwd + '\\foo\\')
18151872

18161873
def test_glob(self):
18171874
P = self.cls

Lib/test/test_pathlib/test_pathlib_abc.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,12 @@ def test_parent_common(self):
365365
self.assertEqual(p.parent.parent, P('/a'))
366366
self.assertEqual(p.parent.parent.parent, P('/'))
367367
self.assertEqual(p.parent.parent.parent.parent, P('/'))
368+
# Trailing sep
369+
p = P('/a/b/c/')
370+
self.assertEqual(p.parent, P('/a/b'))
371+
self.assertEqual(p.parent.parent, P('/a'))
372+
self.assertEqual(p.parent.parent.parent, P('/'))
373+
self.assertEqual(p.parent.parent.parent.parent, P('/'))
368374

369375
def test_parents_common(self):
370376
# Relative
@@ -412,6 +418,27 @@ def test_parents_common(self):
412418
par[-4]
413419
with self.assertRaises(IndexError):
414420
par[3]
421+
# Trailing sep
422+
p = P('/a/b/c/')
423+
par = p.parents
424+
self.assertEqual(len(par), 3)
425+
self.assertEqual(par[0], P('/a/b'))
426+
self.assertEqual(par[1], P('/a'))
427+
self.assertEqual(par[2], P('/'))
428+
self.assertEqual(par[-1], P('/'))
429+
self.assertEqual(par[-2], P('/a'))
430+
self.assertEqual(par[-3], P('/a/b'))
431+
self.assertEqual(par[0:1], (P('/a/b'),))
432+
self.assertEqual(par[:2], (P('/a/b'), P('/a')))
433+
self.assertEqual(par[:-1], (P('/a/b'), P('/a')))
434+
self.assertEqual(par[1:], (P('/a'), P('/')))
435+
self.assertEqual(par[::2], (P('/a/b'), P('/')))
436+
self.assertEqual(par[::-1], (P('/'), P('/a'), P('/a/b')))
437+
self.assertEqual(list(par), [P('/a/b'), P('/a'), P('/')])
438+
with self.assertRaises(IndexError):
439+
par[-4]
440+
with self.assertRaises(IndexError):
441+
par[3]
415442

416443
def test_drive_common(self):
417444
P = self.cls
@@ -441,10 +468,13 @@ def test_name_common(self):
441468
self.assertEqual(P('.').name, '')
442469
self.assertEqual(P('/').name, '')
443470
self.assertEqual(P('a/b').name, 'b')
471+
self.assertEqual(P('a/b/').name, 'b')
444472
self.assertEqual(P('/a/b').name, 'b')
473+
self.assertEqual(P('/a/b/').name, 'b')
445474
self.assertEqual(P('/a/b/.').name, 'b')
446475
self.assertEqual(P('a/b.py').name, 'b.py')
447476
self.assertEqual(P('/a/b.py').name, 'b.py')
477+
self.assertEqual(P('/etc/cron.d/').name, 'cron.d')
448478

449479
def test_suffix_common(self):
450480
P = self.cls
@@ -453,7 +483,9 @@ def test_suffix_common(self):
453483
self.assertEqual(P('..').suffix, '')
454484
self.assertEqual(P('/').suffix, '')
455485
self.assertEqual(P('a/b').suffix, '')
486+
self.assertEqual(P('a/b/').suffix, '')
456487
self.assertEqual(P('/a/b').suffix, '')
488+
self.assertEqual(P('/a/b/').suffix, '')
457489
self.assertEqual(P('/a/b/.').suffix, '')
458490
self.assertEqual(P('a/b.py').suffix, '.py')
459491
self.assertEqual(P('/a/b.py').suffix, '.py')
@@ -465,14 +497,17 @@ def test_suffix_common(self):
465497
self.assertEqual(P('/a/b.tar.gz').suffix, '.gz')
466498
self.assertEqual(P('a/Some name. Ending with a dot.').suffix, '')
467499
self.assertEqual(P('/a/Some name. Ending with a dot.').suffix, '')
500+
self.assertEqual(P('/etc/cron.d/').suffix, '.d')
468501

469502
def test_suffixes_common(self):
470503
P = self.cls
471504
self.assertEqual(P('').suffixes, [])
472505
self.assertEqual(P('.').suffixes, [])
473506
self.assertEqual(P('/').suffixes, [])
474507
self.assertEqual(P('a/b').suffixes, [])
508+
self.assertEqual(P('a/b/').suffixes, [])
475509
self.assertEqual(P('/a/b').suffixes, [])
510+
self.assertEqual(P('/a/b/').suffixes, [])
476511
self.assertEqual(P('/a/b/.').suffixes, [])
477512
self.assertEqual(P('a/b.py').suffixes, ['.py'])
478513
self.assertEqual(P('/a/b.py').suffixes, ['.py'])
@@ -484,6 +519,7 @@ def test_suffixes_common(self):
484519
self.assertEqual(P('/a/b.tar.gz').suffixes, ['.tar', '.gz'])
485520
self.assertEqual(P('a/Some name. Ending with a dot.').suffixes, [])
486521
self.assertEqual(P('/a/Some name. Ending with a dot.').suffixes, [])
522+
self.assertEqual(P('/etc/cron.d/').suffixes, ['.d'])
487523

488524
def test_stem_common(self):
489525
P = self.cls
@@ -492,12 +528,14 @@ def test_stem_common(self):
492528
self.assertEqual(P('..').stem, '..')
493529
self.assertEqual(P('/').stem, '')
494530
self.assertEqual(P('a/b').stem, 'b')
531+
self.assertEqual(P('a/b/').stem, 'b')
495532
self.assertEqual(P('a/b.py').stem, 'b')
496533
self.assertEqual(P('a/.hgrc').stem, '.hgrc')
497534
self.assertEqual(P('a/.hg.rc').stem, '.hg')
498535
self.assertEqual(P('a/b.tar.gz').stem, 'b.tar')
499536
self.assertEqual(P('a/Some name. Ending with a dot.').stem,
500537
'Some name. Ending with a dot.')
538+
self.assertEqual(P('/etc/cron.d/').stem, 'cron')
501539

502540
def test_with_name_common(self):
503541
P = self.cls
@@ -507,6 +545,7 @@ def test_with_name_common(self):
507545
self.assertEqual(P('/a/b.py').with_name('d.xml'), P('/a/d.xml'))
508546
self.assertEqual(P('a/Dot ending.').with_name('d.xml'), P('a/d.xml'))
509547
self.assertEqual(P('/a/Dot ending.').with_name('d.xml'), P('/a/d.xml'))
548+
self.assertEqual(P('/etc/cron.d/').with_name('tron.g'), P('/etc/tron.g/'))
510549
self.assertRaises(ValueError, P('').with_name, 'd.xml')
511550
self.assertRaises(ValueError, P('.').with_name, 'd.xml')
512551
self.assertRaises(ValueError, P('/').with_name, 'd.xml')
@@ -525,6 +564,7 @@ def test_with_stem_common(self):
525564
self.assertEqual(P('/a/b.tar.gz').with_stem('d'), P('/a/d.gz'))
526565
self.assertEqual(P('a/Dot ending.').with_stem('d'), P('a/d'))
527566
self.assertEqual(P('/a/Dot ending.').with_stem('d'), P('/a/d'))
567+
self.assertEqual(P('/etc/cron.d/').with_stem('tron'), P('/etc/tron.d/'))
528568
self.assertRaises(ValueError, P('').with_stem, 'd')
529569
self.assertRaises(ValueError, P('.').with_stem, 'd')
530570
self.assertRaises(ValueError, P('/').with_stem, 'd')
@@ -540,9 +580,11 @@ def test_with_suffix_common(self):
540580
self.assertEqual(P('/a/b').with_suffix('.gz'), P('/a/b.gz'))
541581
self.assertEqual(P('a/b.py').with_suffix('.gz'), P('a/b.gz'))
542582
self.assertEqual(P('/a/b.py').with_suffix('.gz'), P('/a/b.gz'))
583+
self.assertEqual(P('/etc/cron.d/').with_suffix('.g'), P('/etc/cron.g/'))
543584
# Stripping suffix.
544585
self.assertEqual(P('a/b.py').with_suffix(''), P('a/b'))
545586
self.assertEqual(P('/a/b').with_suffix(''), P('/a/b'))
587+
self.assertEqual(P('/etc/cron.d/').with_suffix(''), P('/etc/cron/'))
546588
# Path doesn't have a "filename" component.
547589
self.assertRaises(ValueError, P('').with_suffix, '.gz')
548590
self.assertRaises(ValueError, P('.').with_suffix, '.gz')
@@ -636,6 +678,25 @@ def test_relative_to_common(self):
636678
self.assertRaises(ValueError, p.relative_to, P("a/.."), walk_up=True)
637679
self.assertRaises(ValueError, p.relative_to, P("/a/.."), walk_up=True)
638680

681+
def test_relative_to_trailing_sep(self):
682+
P = self.cls
683+
self.assertEqual(P('foo').relative_to('foo'), P())
684+
self.assertEqual(P('foo').relative_to('foo/'), P())
685+
self.assertEqual(P('foo/').relative_to('foo'), P())
686+
self.assertEqual(P('foo/').relative_to('foo/'), P())
687+
self.assertEqual(P('foo/bar').relative_to('foo'), P('bar'))
688+
self.assertEqual(P('foo/bar').relative_to('foo/'), P('bar'))
689+
self.assertEqual(P('foo/bar/').relative_to('foo'), P('bar/'))
690+
self.assertEqual(P('foo/bar/').relative_to('foo/'), P('bar/'))
691+
self.assertEqual(P('foo').relative_to('foo/bar', walk_up=True), P('..'))
692+
self.assertEqual(P('foo').relative_to('foo/bar/', walk_up=True), P('..'))
693+
self.assertEqual(P('foo/').relative_to('foo/bar', walk_up=True), P('../'))
694+
self.assertEqual(P('foo/').relative_to('foo/bar/', walk_up=True), P('../'))
695+
self.assertEqual(P('foo/oof').relative_to('foo/bar', walk_up=True), P('../oof'))
696+
self.assertEqual(P('foo/oof').relative_to('foo/bar/', walk_up=True), P('../oof'))
697+
self.assertEqual(P('foo/oof/').relative_to('foo/bar', walk_up=True), P('../oof/'))
698+
self.assertEqual(P('foo/oof/').relative_to('foo/bar/', walk_up=True), P('../oof/'))
699+
639700
def test_is_relative_to_common(self):
640701
P = self.cls
641702
p = P('a/b')
@@ -671,6 +732,25 @@ def test_is_relative_to_common(self):
671732
self.assertFalse(p.is_relative_to(''))
672733
self.assertFalse(p.is_relative_to(P('a')))
673734

735+
def test_is_relative_to_trailing_sep(self):
736+
P = self.cls
737+
self.assertTrue(P('foo').is_relative_to('foo'))
738+
self.assertTrue(P('foo').is_relative_to('foo/'))
739+
self.assertTrue(P('foo/').is_relative_to('foo'))
740+
self.assertTrue(P('foo/').is_relative_to('foo/'))
741+
self.assertTrue(P('foo/bar').is_relative_to('foo'))
742+
self.assertTrue(P('foo/bar').is_relative_to('foo/'))
743+
self.assertTrue(P('foo/bar/').is_relative_to('foo'))
744+
self.assertTrue(P('foo/bar/').is_relative_to('foo/'))
745+
self.assertFalse(P('foo').is_relative_to('foo/bar'))
746+
self.assertFalse(P('foo').is_relative_to('foo/bar/'))
747+
self.assertFalse(P('foo/').is_relative_to('foo/bar'))
748+
self.assertFalse(P('foo/').is_relative_to('foo/bar/'))
749+
self.assertFalse(P('foo/oof').is_relative_to('foo/bar'))
750+
self.assertFalse(P('foo/oof').is_relative_to('foo/bar/'))
751+
self.assertFalse(P('foo/oof/').is_relative_to('foo/bar'))
752+
self.assertFalse(P('foo/oof/').is_relative_to('foo/bar/'))
753+
674754

675755
#
676756
# Tests for the virtual classes.

0 commit comments

Comments
 (0)