Skip to content

GH-130614: pathlib ABCs: revise test suite for Windows path joining #131016

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 2 commits into from
Mar 11, 2025
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
6 changes: 6 additions & 0 deletions Lib/test/test_pathlib/support/lexical_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Simple implementation of JoinablePath, for use in pathlib tests.
"""

import ntpath
import os.path
import pathlib.types
import posixpath
Expand Down Expand Up @@ -37,3 +38,8 @@ def with_segments(self, *pathsegments):
class LexicalPosixPath(LexicalPath):
__slots__ = ()
parser = posixpath


class LexicalWindowsPath(LexicalPath):
__slots__ = ()
parser = ntpath
290 changes: 290 additions & 0 deletions Lib/test/test_pathlib/test_join_windows.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
"""
Tests for Windows-flavoured pathlib.types._JoinablePath
"""

import os
import unittest

from pathlib import PureWindowsPath, WindowsPath
from test.test_pathlib.support.lexical_path import LexicalWindowsPath


class JoinTestBase:
def test_join(self):
P = self.cls
p = P('C:/a/b')
pp = p.joinpath('x/y')
self.assertEqual(pp, P(r'C:/a/b\x/y'))
pp = p.joinpath('/x/y')
self.assertEqual(pp, P('C:/x/y'))
# Joining with a different drive => the first path is ignored, even
# if the second path is relative.
pp = p.joinpath('D:x/y')
self.assertEqual(pp, P('D:x/y'))
pp = p.joinpath('D:/x/y')
self.assertEqual(pp, P('D:/x/y'))
pp = p.joinpath('//host/share/x/y')
self.assertEqual(pp, P('//host/share/x/y'))
# Joining with the same drive => the first path is appended to if
# the second path is relative.
pp = p.joinpath('c:x/y')
self.assertEqual(pp, P(r'c:/a/b\x/y'))
pp = p.joinpath('c:/x/y')
self.assertEqual(pp, P('c:/x/y'))
# Joining with files with NTFS data streams => the filename should
# not be parsed as a drive letter
pp = p.joinpath('./d:s')
self.assertEqual(pp, P(r'C:/a/b\./d:s'))
pp = p.joinpath('./dd:s')
self.assertEqual(pp, P(r'C:/a/b\./dd:s'))
pp = p.joinpath('E:d:s')
self.assertEqual(pp, P('E:d:s'))
# Joining onto a UNC path with no root
pp = P('//').joinpath('server')
self.assertEqual(pp, P('//server'))
pp = P('//server').joinpath('share')
self.assertEqual(pp, P(r'//server\share'))
pp = P('//./BootPartition').joinpath('Windows')
self.assertEqual(pp, P(r'//./BootPartition\Windows'))

def test_div(self):
# Basically the same as joinpath().
P = self.cls
p = P('C:/a/b')
self.assertEqual(p / 'x/y', P(r'C:/a/b\x/y'))
self.assertEqual(p / 'x' / 'y', P(r'C:/a/b\x\y'))
self.assertEqual(p / '/x/y', P('C:/x/y'))
self.assertEqual(p / '/x' / 'y', P('C:/x\y'))
# Joining with a different drive => the first path is ignored, even
# if the second path is relative.
self.assertEqual(p / 'D:x/y', P('D:x/y'))
self.assertEqual(p / 'D:' / 'x/y', P('D:x/y'))
self.assertEqual(p / 'D:/x/y', P('D:/x/y'))
self.assertEqual(p / 'D:' / '/x/y', P('D:/x/y'))
self.assertEqual(p / '//host/share/x/y', P('//host/share/x/y'))
# Joining with the same drive => the first path is appended to if
# the second path is relative.
self.assertEqual(p / 'c:x/y', P(r'c:/a/b\x/y'))
self.assertEqual(p / 'c:/x/y', P('c:/x/y'))
# Joining with files with NTFS data streams => the filename should
# not be parsed as a drive letter
self.assertEqual(p / './d:s', P(r'C:/a/b\./d:s'))
self.assertEqual(p / './dd:s', P(r'C:/a/b\./dd:s'))
self.assertEqual(p / 'E:d:s', P('E:d:s'))

def test_str(self):
p = self.cls(r'a\b\c')
self.assertEqual(str(p), 'a\\b\\c')
p = self.cls(r'c:\a\b\c')
self.assertEqual(str(p), 'c:\\a\\b\\c')
p = self.cls('\\\\a\\b\\')
self.assertEqual(str(p), '\\\\a\\b\\')
p = self.cls(r'\\a\b\c')
self.assertEqual(str(p), '\\\\a\\b\\c')
p = self.cls(r'\\a\b\c\d')
self.assertEqual(str(p), '\\\\a\\b\\c\\d')

def test_parts(self):
P = self.cls
p = P(r'c:a\b')
parts = p.parts
self.assertEqual(parts, ('c:', 'a', 'b'))
p = P(r'c:\a\b')
parts = p.parts
self.assertEqual(parts, ('c:\\', 'a', 'b'))
p = P(r'\\a\b\c\d')
parts = p.parts
self.assertEqual(parts, ('\\\\a\\b\\', 'c', 'd'))

def test_parent(self):
# Anchored
P = self.cls
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:'))
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:/'))
p = P('//a/b/c/d')
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/'))

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)
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('//a/b/c/d')
par = p.parents
self.assertEqual(len(par), 2)
self.assertEqual(par[0], P('//a/b/c'))
self.assertEqual(par[1], P('//a/b/'))
self.assertEqual(par[0:1], (P('//a/b/c'),))
self.assertEqual(par[0:-1], (P('//a/b/c'),))
self.assertEqual(par[:2], (P('//a/b/c'), P('//a/b/')))
self.assertEqual(par[1:], (P('//a/b/'),))
self.assertEqual(par[::2], (P('//a/b/c'),))
self.assertEqual(par[::-1], (P('//a/b/'), P('//a/b/c')))
self.assertEqual(list(par), [P('//a/b/c'), P('//a/b/')])
with self.assertRaises(IndexError):
par[2]

def test_anchor(self):
P = self.cls
self.assertEqual(P('c:').anchor, 'c:')
self.assertEqual(P('c:a/b').anchor, 'c:')
self.assertEqual(P('c:\\').anchor, 'c:\\')
self.assertEqual(P('c:\\a\\b\\').anchor, 'c:\\')
self.assertEqual(P('\\\\a\\b\\').anchor, '\\\\a\\b\\')
self.assertEqual(P('\\\\a\\b\\c\\d').anchor, '\\\\a\\b\\')

def test_name(self):
P = self.cls
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.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')

def test_stem(self):
P = self.cls
self.assertEqual(P('c:').stem, '')
self.assertEqual(P('c:..').stem, '..')
self.assertEqual(P('c:/').stem, '')
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/trailing.dot.').stem, 'trailing.dot')

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.py').suffix, '.py')
self.assertEqual(P('c:/a/b.py').suffix, '.py')
self.assertEqual(P('c:a/.hgrc').suffix, '')
self.assertEqual(P('c:/a/.hgrc').suffix, '')
self.assertEqual(P('c:a/.hg.rc').suffix, '.rc')
self.assertEqual(P('c:/a/.hg.rc').suffix, '.rc')
self.assertEqual(P('c:a/b.tar.gz').suffix, '.gz')
self.assertEqual(P('c:/a/b.tar.gz').suffix, '.gz')
self.assertEqual(P('c:a/trailing.dot.').suffix, '.')
self.assertEqual(P('c:/a/trailing.dot.').suffix, '.')
self.assertEqual(P('//My.py/Share.php').suffix, '')
self.assertEqual(P('//My.py/Share.php/a/b').suffix, '')

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.py').suffixes, ['.py'])
self.assertEqual(P('c:/a/b.py').suffixes, ['.py'])
self.assertEqual(P('c:a/.hgrc').suffixes, [])
self.assertEqual(P('c:/a/.hgrc').suffixes, [])
self.assertEqual(P('c:a/.hg.rc').suffixes, ['.rc'])
self.assertEqual(P('c:/a/.hg.rc').suffixes, ['.rc'])
self.assertEqual(P('c:a/b.tar.gz').suffixes, ['.tar', '.gz'])
self.assertEqual(P('c:/a/b.tar.gz').suffixes, ['.tar', '.gz'])
self.assertEqual(P('//My.py/Share.php').suffixes, [])
self.assertEqual(P('//My.py/Share.php/a/b').suffixes, [])
self.assertEqual(P('c:a/trailing.dot.').suffixes, ['.dot', '.'])
self.assertEqual(P('c:/a/trailing.dot.').suffixes, ['.dot', '.'])

def test_with_name(self):
P = self.cls
self.assertEqual(P(r'c:a\b').with_name('d.xml'), P(r'c:a\d.xml'))
self.assertEqual(P(r'c:\a\b').with_name('d.xml'), P(r'c:\a\d.xml'))
self.assertEqual(P(r'c:a\Dot ending.').with_name('d.xml'), P(r'c:a\d.xml'))
self.assertEqual(P(r'c:\a\Dot ending.').with_name('d.xml'), P(r'c:\a\d.xml'))
self.assertRaises(ValueError, P(r'c:a\b').with_name, r'd:\e')
self.assertRaises(ValueError, P(r'c:a\b').with_name, r'\\My\Share')

def test_with_stem(self):
P = self.cls
self.assertEqual(P('c:a/b').with_stem('d'), P('c:a/d'))
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.assertRaises(ValueError, P('c:a/b').with_stem, 'd:/e')
self.assertRaises(ValueError, P('c:a/b').with_stem, '//My/Share')

def test_with_suffix(self):
P = self.cls
self.assertEqual(P('c:a/b').with_suffix('.gz'), P('c:a/b.gz'))
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'))
# Path doesn't have a "filename" component.
self.assertRaises(ValueError, P('').with_suffix, '.gz')
self.assertRaises(ValueError, P('/').with_suffix, '.gz')
self.assertRaises(ValueError, P('//My/Share').with_suffix, '.gz')
# Invalid suffix.
self.assertRaises(ValueError, P('c:a/b').with_suffix, 'gz')
self.assertRaises(ValueError, P('c:a/b').with_suffix, '/')
self.assertRaises(ValueError, P('c:a/b').with_suffix, '\\')
self.assertRaises(ValueError, P('c:a/b').with_suffix, 'c:')
self.assertRaises(ValueError, P('c:a/b').with_suffix, '/.gz')
self.assertRaises(ValueError, P('c:a/b').with_suffix, '\\.gz')
self.assertRaises(ValueError, P('c:a/b').with_suffix, 'c:.gz')
self.assertRaises(ValueError, P('c:a/b').with_suffix, 'c/d')
self.assertRaises(ValueError, P('c:a/b').with_suffix, 'c\\d')
self.assertRaises(ValueError, P('c:a/b').with_suffix, '.c/d')
self.assertRaises(ValueError, P('c:a/b').with_suffix, '.c\\d')
self.assertRaises(TypeError, P('c:a/b').with_suffix, None)


class LexicalWindowsPathJoinTest(JoinTestBase, unittest.TestCase):
cls = LexicalWindowsPath


class PureWindowsPathJoinTest(JoinTestBase, unittest.TestCase):
cls = PureWindowsPath


if os.name == 'nt':
class WindowsPathJoinTest(JoinTestBase, unittest.TestCase):
cls = WindowsPath


if __name__ == "__main__":
unittest.main()
24 changes: 24 additions & 0 deletions Lib/test/test_pathlib/test_pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,18 @@ def test_stem_empty(self):
self.assertEqual(P('').stem, '')
self.assertEqual(P('.').stem, '')

@needs_windows
def test_with_name_windows(self):
P = self.cls
self.assertRaises(ValueError, P(r'c:').with_name, 'd.xml')
self.assertRaises(ValueError, P(r'c:\\').with_name, 'd.xml')
self.assertRaises(ValueError, P(r'\\My\Share').with_name, 'd.xml')
# NTFS alternate data streams
self.assertEqual(str(P('a').with_name('d:')), '.\\d:')
self.assertEqual(str(P('a').with_name('d:e')), '.\\d:e')
self.assertEqual(P(r'c:a\b').with_name('d:'), P(r'c:a\d:'))
self.assertEqual(P(r'c:a\b').with_name('d:e'), P(r'c:a\d:e'))

def test_with_name_empty(self):
P = self.cls
self.assertRaises(ValueError, P('').with_name, 'd.xml')
Expand All @@ -419,6 +431,18 @@ def test_with_name_empty(self):
self.assertRaises(ValueError, P('a/b').with_name, '')
self.assertRaises(ValueError, P('a/b').with_name, '.')

@needs_windows
def test_with_stem_windows(self):
P = self.cls
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')
# NTFS alternate data streams
self.assertEqual(str(P('a').with_stem('d:')), '.\\d:')
self.assertEqual(str(P('a').with_stem('d:e')), '.\\d:e')
self.assertEqual(P('c:a/b').with_stem('d:'), P('c:a/d:'))
self.assertEqual(P('c:a/b').with_stem('d:e'), P('c:a/d:e'))

def test_with_stem_empty(self):
P = self.cls
self.assertRaises(ValueError, P('').with_stem, 'd')
Expand Down
Loading
Loading