8000 gh-101000: Add os.path.splitroot() by barneygale · Pull Request #101002 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content

gh-101000: Add os.path.splitroot() #101002

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 31 commits into from
Jan 27, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
21c0ba9
gh-101000: Add os.path.splitroot()
barneygale Jan 12, 2023
836b85d
Use splitroot() from pathlib
barneygale Jan 12, 2023
bc2d1f9
Use splitroot() from posixpath
barneygale Jan 12, 2023
ecdc40d
Use splitroot() from ntpath
barneygale Jan 12, 2023
6592b27
Optimizations
barneygale Jan 12, 2023
78f4227
Correct and expand examples in splitroot() docstring
barneygale Jan 13, 2023
9726ca4
Update Lib/ntpath.py
barneygale Jan 13, 2023
7a6613c
Use splitroot() from pathlib.PurePath.with_name()
barneygale Jan 14, 2023
26a8dba
Reduce ntpath.normpath() diff noise
barneygale Jan 15, 2023
0c237d4
Simplify ntpath.commonpath() now that 'isabs' is unused.
barneygale Jan 15, 2023
11ed3eb
Reduce posixpath.normpath() diff noise
barneygale Jan 15, 2023
2c9eed8
Improve documentation
barneygale Jan 15, 2023
8299e96
Add whatsnew entry.
barneygale Jan 15, 2023
27ffe37
Simplify ntpath.splitroot() slightly
barneygale Jan 15, 2023
9beff2a
Apply suggestions from code review
barneygale Jan 16, 2023
bacdee1
Update Doc/library/os.path.rst
barneygale Jan 16, 2023
4ebe545
Note that drive may be empty on Windows
barneygale Jan 16, 2023
2927afe
Re-order drive example
barneygale Jan 16, 2023
b0aa73e
Update Doc/library/os.path.rst
barneygale Jan 16, 2023
19777d6
Adjust docstring examples
barneygale Jan 18, 2023
32e212e
Apply suggestions from code review
barneygale Jan 19, 2023
37cded3
Update Doc/library/os.path.rst
barneygale Jan 19, 2023
5a8dfce
Change example username in docs to 'Sam'
barneygale Jan 19, 2023
0e75a55
Adjust first paragraph to use prose
barneygale Jan 19, 2023
3663237
Update Doc/library/os.path.rst
barneygale Jan 22, 2023
053729d
Add tests for bytes (POSIX only) and path-like objects (both platforms)
barneygale Jan 22, 2023
694f093
Add tests for mixed path separators (Windows only)
barneygale Jan 22, 2023
e99e3cd
Remove errant newline.
barneygale Jan 22, 2023
f618a00
Move most test cases from `test_splitdrive` to `test_splitroot`
barneygale Jan 22, 2023
1c522c9
Mention pathlib performance improvement in news entry.
barneygale Jan 22, 2023
df17269
Merge branch 'main' into gh-101000-splitroot
AlexWaygood Jan 22, 2023
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
Next Next commit
gh-101000: Add os.path.splitroot()
  • Loading branch information
barneygale committed Jan 12, 2023
commit 21c0ba9069f496912f6cc6fc31f21055b772d1ea
20 changes: 20 additions & 0 deletions Doc/library/os.path.rst
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,26 @@ the :mod:`glob` module.)
Accepts a :term:`path-like object`.


.. function:: splitroot(path)

Split the pathname *path* into a triad ``(drive, root, tail)`` where:

1. *drive* is an optional mount point, exactly like :func:`splitdrive`;
2. *root* is an optional sequence of separators following the drive; and
3. *tail* is anything after the root.

On Posix, *drive* is always empty. The *root* may be empty (relative path),
a single forward slash (absolute path), or two forward slashes
(implementation-defined per the POSIX standard).

On Windows, *drive* may be a UNC sharepoint or a traditional DOS drive. The
*root* may be empty, a forward slash, or a backward slash.

In all cases, ``drive + root + tail`` will be the same as *path*.

.. versionadded:: 3.12


.. function:: splitext(path)

Split the pathname *path* into a pair ``(root, ext)`` such that ``root + ext ==
Expand Down
65 changes: 44 additions & 21 deletions Lib/ntpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from genericpath import *


__all__ = ["normcase","isabs","join","splitdrive","split","splitext",
__all__ = ["normcase","isabs","join","splitdrive","splitroot","split","splitext",
"basename","dirname","commonprefix","getsize","getmtime",
"getatime","getctime", "islink","exists","lexists","isdir","isfile",
"ismount", "expanduser","expandvars","normpath","abspath",
Expand Down Expand Up @@ -169,35 +169,58 @@ def splitdrive(p):

Paths cannot contain both a drive letter and a UNC path.

"""
drive, root, tail = splitroot(p)
return drive, root + tail


def splitroot(p):
"""Split a pathname into drive, root and tail. The drive is defined
exactly as in splitdrive(). On Windows, the root may be a single path
separator or an empty string. The tail contains anything after the root.
For example:

splitroot('//server/share/') == ('//server/share', '/', '')
splitroot('C:/Users/Barney') == ('C:', '/', 'Users/Barney')
splitroot('Windows') == ('', '', 'Windows')
"""
p = os.fspath(p)
if len(p) >= 2:
if isinstance(p, bytes):
sep = b'\\'
altsep = b'/'
colon = b':'
unc_prefix = b'\\\\?\\UNC\\'
else:
sep = '\\'
altsep = '/'
colon = ':'
unc_prefix = '\\\\?\\UNC\\'
normp = p.replace(altsep, sep)
if normp[0:2] == sep * 2:
if isinstance(p, bytes):
sep = b'\\'
altsep = b'/'
colon = b':'
unc_prefix = b'\\\ 8000 \?\\UNC\\'
else:
sep = '\\'
altsep = '/'
colon = ':'
unc_prefix = '\\\\?\\UNC\\'
normp = p.replace(altsep, sep)
if normp[:1] == sep:
if normp[1:2] == sep:
# UNC drives, e.g. \\server\share or \\?\UNC\server\share
# Device drives, e.g. \\.\device or \\?\device
start = 8 if normp[:8].upper() == unc_prefix else 2
index = normp.find(sep, start)
if index == -1:
return p, p[:0]
return p, p[:0], p[:0]
index2 = normp.find(sep, index + 1)
if index2 == -1:
return p, p[:0]
return p[:index2], p[index2:]
if normp[1:2] == colon:
# Drive-letter drives, e.g. X:
return p[:2], p[2:]
return p[:0], p
return p, p[:0], p[:0]
return p[:index2], p[index2:index2 + 1], p[index2 + 1:]
else:
# Relative path with root, e.g. \Windows
return p[:0], p[:1], p[1:]
elif normp[1:2] == colon:
if normp[2:3] == sep:
# Absolute drive-letter path, e.g. X:\Windows
return p[:2], p[2:3], p[3:]
else:
# Relative path with drive, e.g. X:Windows
return p[:2], p[:0], p[2:]
else:
# Relative path, e.g. Windows
return p[:0], p[:0], p


# Split a path in head (everything up to the last '/') and tail (the
Expand Down
26 changes: 25 additions & 1 deletion Lib/posixpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import genericpath
from genericpath import *

__all__ = ["normcase","isabs","join","splitdrive","split","splitext",
__all__ = ["normcase","isabs","join","splitdrive","splitroot","split","splitext",
"basename","dirname","commonprefix","getsize","getmtime",
"getatime","getctime","islink","exists","lexists","isdir","isfile",
"ismount", "expanduser","expandvars","normpath","abspath",
Expand Down Expand Up @@ -135,6 +135,30 @@ def splitdrive(p):
return p[:0], p


def splitroot(p):
"""Split a pathname into drive, root and tail. On Posix, drive is always
empty; the root may be empty, a single slash, or two slashes. The tail
contains anything after the root. For example:

splitdrive('foo/bar& 8000 #39;) == ('', '', 'foo/bar')
splitdrive('/foo/bar') == ('', '/', 'foo/bar')
"""
p = os.fspath(p)
sep = b'/' if isinstance(p, bytes) else '/'
if p[:1] != sep:
# Relative path, e.g.: 'foo'
return p[:0], p[:0], p
elif p[1:2] != sep:
# Absolute path, e.g.: '/foo'
return p[:0], p[:1], p[1:]
elif p[2:3] != sep:
# Implementation defined per POSIX standard, e.g.: '//foo'
return p[:0], p[:2], p[2:]
else:
# Absolute path with extraneous slashes, e.g.: '///foo', '////foo', etc.
return p[:0], p[:1], p[1:]


# Return the tail (basename) part of a path, same as split(path)[1].

def basename(p):
Expand Down
16 changes: 16 additions & 0 deletions Lib/test/test_ntpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,22 @@ def test_splitdrive(self):
tester('ntpath.splitdrive("//x")', ("//x", "")) # non-empty server & missing share
tester('ntpath.splitdrive("//x/")', ("//x/", "")) # non-empty server & empty share

def test_splitroot(self):
tester("ntpath.splitroot('')", ('', '', ''))
tester("ntpath.splitroot('a')", ('', '', 'a'))
tester("ntpath.splitroot('a\\b')", ('', '', 'a\\b'))
tester("ntpath.splitroot('\\a')", ('', '\\', 'a'))
tester("ntpath.splitroot('\\a\\b')", ('', '\\', 'a\\b'))
tester("ntpath.splitroot('c:a\\b')", ('c:', '', 'a\\b'))
tester("ntpath.splitroot('c:\\a\\b')", ('c:', '\\', 'a\\b'))
# Redundant slashes are not included in the root.
tester("ntpath.splitroot('c:\\\\a')", ('c:', '\\', '\\a'))
tester("ntpath.splitroot('c:\\\\\\a/b')", ('c:', '\\', '\\\\a/b'))
# Valid UNC paths.
tester("ntpath.splitroot('\\\\a\\b')", ('\\\\a\\b', '', ''))
tester("ntpath.splitroot('\\\\a\\b\\')", ('\\\\a\\b', '\\', ''))
tester("ntpath.splitroot('\\\\a\\b\\c\\d')", ('\\\\a\\b', '\\', 'c\\d'))

def test_split(self):
tester('ntpath.split("c:\\foo\\bar")', ('c:\\foo', 'bar'))
tester('ntpath.split("\\\\conky\\mountpoint\\foo\\bar")',
Expand Down
20 changes: 20 additions & 0 deletions Lib/test/test_posixpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,26 @@ def test_splitext(self):
self.splitextTest("........", "........", "")
self.splitextTest("", "", "")

def test_splitroot(self):
f = posixpath.splitroot
self.assertEqual(f(''), ('', '', ''))
self.assertEqual(f('a'), ('', '', 'a'))
self.assertEqual(f('a/b'), ('', '', 'a/b'))
self.assertEqual(f('a/b/'), ('', '', 'a/b/'))
self.assertEqual(f('/a'), ('', '/', 'a'))
self.assertEqual(f('/a/b'), ('', '/', 'a/b'))
self.assertEqual(f('/a/b/'), ('', '/', 'a/b/'))
# The root is collapsed when there are redundant slashes
# except when there are exactly two leading slashes, which
# is a special case in POSIX.
self.assertEqual(f('//a'), ('', '//', 'a'))
self.assertEqual(f('///a'), ('', '/', '//a'))
self.assertEqual(f('///a/b'), ('', '/', '//a/b'))
# Paths which look like NT paths aren't treated specially.
self.assertEqual(f('c:/a/b'), ('', '', 'c:/a/b'))
self.assertEqual(f('\\/a/b'), ('', '', '\\/a/b'))
self.assertEqual(f('\\a\\b'), ('', '', '\\a\\b'))

def test_isabs(self):
self.assertIs(posixpath.isabs(""), False)
self.assertIs(posixpath.isabs("/"), True)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add :func:`os.path.splitroot()`, which splits a path into a triad of
``(drive, root, tail)``.
0