Skip to content

Commit

Permalink
pythongh-101000: Add os.path.splitroot()
Browse files Browse the repository at this point in the history
  • Loading branch information
barneygale committed Jan 12, 2023
1 parent 005e694 commit 21c0ba9
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 22 deletions.
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'\\\\?\\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') == ('', '', '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 comments on commit 21c0ba9

Please sign in to comment.