Skip to content
Closed
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
23 changes: 17 additions & 6 deletions Doc/library/os.path.rst
Original file line number Diff line number Diff line change
Expand Up @@ -332,17 +332,28 @@ the :mod:`glob` module.)
Accepts a :term:`path-like object`.


.. function:: normpath(path)
.. function:: normpath(path, *, keep_curdir=False, keep_pardir=False)

Normalize a pathname by collapsing redundant separators and up-level
references so that ``A//B``, ``A/B/``, ``A/./B`` and ``A/foo/../B`` all
become ``A/B``. This string manipulation may change the meaning of a path
that contains symbolic links. On Windows, it converts forward slashes to
backward slashes. To normalize case, use :func:`normcase`.
Normalize a pathname by collapsing redundant separators so that ``A//B`` and
``A/B/`` become ``A/B``.

Unless *keep_curdir* is ``True``, references to the current directory are
collapsed so that ``./A/./B`` becomes ``A/B``. This may change how the path
is interpreted by a shell.

Unless *keep_pardir* is ``True``, references to the parent directory are
collapsed so that ``A/foo/../B`` becomes ``A/B``. This string manipulation
may change the meaning of a path that contains symbolic links.

On Windows, this function converts forward slashes to backward slashes. To
normalize case, use :func:`normcase`.

.. versionchanged:: 3.6
Accepts a :term:`path-like object`.

.. versionchanged:: 3.11
The *strict* parameter was added.


.. function:: realpath(path, *, strict=False)

Expand Down
14 changes: 8 additions & 6 deletions Lib/ntpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -460,20 +460,20 @@ def expandvars(path):
# Previously, this function also truncated pathnames to 8+3 format,
# but as this module is called "ntpath", that's obviously wrong!

def normpath(path):
def normpath(path, *, keep_curdir=False, keep_pardir=False):
"""Normalize path, eliminating double slashes, etc."""
path = os.fspath(path)
if isinstance(path, bytes):
sep = b'\\'
altsep = b'/'
curdir = b'.'
pardir = b'..'
dot = b'.'
dotdot = b'..'
special_prefixes = (b'\\\\.\\', b'\\\\?\\')
else:
sep = '\\'
altsep = '/'
curdir = '.'
pardir = '..'
dot = '.'
dotdot = '..'
special_prefixes = ('\\\\.\\', '\\\\?\\')
if path.startswith(special_prefixes):
# in the case of paths with these prefixes:
Expand All @@ -482,6 +482,8 @@ def normpath(path):
# do not do any normalization, but return the path
# unchanged apart from the call to os.fspath()
return path
curdir = object() if keep_curdir else dot
pardir = object() if keep_pardir else dotdot
path = path.replace(altsep, sep)
prefix, path = splitdrive(path)

Expand All @@ -507,7 +509,7 @@ def normpath(path):
i += 1
# If the path is now empty, substitute '.'
if not prefix and not comps:
comps.append(curdir)
comps.append(dot)
return prefix + sep.join(comps)

def _abspath_fallback(path):
Expand Down
10 changes: 6 additions & 4 deletions Lib/posixpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ def expandvars(path):
# It should be understood that this may change the meaning of the path
# if it contains symbolic links!

def normpath(path):
def normpath(path, *, keep_curdir=False, keep_pardir=False):
"""Normalize path, eliminating double slashes, etc."""
path = os.fspath(path)
if isinstance(path, bytes):
Expand All @@ -349,6 +349,8 @@ def normpath(path):
dotdot = '..'
if path == empty:
return dot
curdir = object() if keep_curdir else dot
pardir = object() if keep_pardir else dotdot
initial_slashes = path.startswith(sep)
# POSIX allows one or two initial slashes, but treats three or more
# as single slash.
Expand All @@ -358,10 +360,10 @@ def normpath(path):
comps = path.split(sep)
new_comps = []
for comp in comps:
if comp in (empty, dot):
if comp in (empty, curdir):
continue
if (comp != dotdot or (not initial_slashes and not new_comps) or
(new_comps and new_comps[-1] == dotdot)):
if (comp != pardir or (not initial_slashes and not new_comps) or
(new_comps and new_comps[-1] == pardir)):
new_comps.append(comp)
elif new_comps:
new_comps.pop()
Expand Down
125 changes: 125 additions & 0 deletions Lib/test/test_ntpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,131 @@ def test_normpath(self):
tester("ntpath.normpath('\\\\.\\NUL')", r'\\.\NUL')
tester("ntpath.normpath('\\\\?\\D:/XY\\Z')", r'\\?\D:/XY\Z')

def test_normpath_keep_curdir(self):
tester("ntpath.normpath('A//////././//.//B', keep_curdir=True)", r'A\.\.\.\B')
tester("ntpath.normpath('A/./B', keep_curdir=True)", r'A\.\B')
tester("ntpath.normpath('A/foo/../B', keep_curdir=True)", r'A\B')
tester("ntpath.normpath('C:A//B', keep_curdir=True)", r'C:A\B')
tester("ntpath.normpath('D:A/./B', keep_curdir=True)", r'D:A\.\B')
tester("ntpath.normpath('e:A/foo/../B', keep_curdir=True)", r'e:A\B')

tester("ntpath.normpath('C:///A//B', keep_curdir=True)", r'C:\A\B')
tester("ntpath.normpath('D:///A/./B', keep_curdir=True)", r'D:\A\.\B')
tester("ntpath.normpath('e:///A/foo/../B', keep_curdir=True)", r'e:\A\B')

tester("ntpath.normpath('..', keep_curdir=True)", r'..')
tester("ntpath.normpath('.', keep_curdir=True)", r'.')
tester("ntpath.normpath('', keep_curdir=True)", r'.')
tester("ntpath.normpath('/', keep_curdir=True)", '\\')
tester("ntpath.normpath('c:/', keep_curdir=True)", 'c:\\')
tester("ntpath.normpath('/../.././..', keep_curdir=True)", '\\')
tester("ntpath.normpath('c:/../../..', keep_curdir=True)", 'c:\\')
tester("ntpath.normpath('../.././..', keep_curdir=True)", r'..\..')
tester("ntpath.normpath('K:../.././..', keep_curdir=True)", r'K:..\..')
tester("ntpath.normpath('C:////a/b', keep_curdir=True)", r'C:\a\b')
tester("ntpath.normpath('//machine/share//a/b', keep_curdir=True)", r'\\machine\share\a\b')

tester("ntpath.normpath('\\\\.\\NUL', keep_curdir=True)", r'\\.\NUL')
tester("ntpath.normpath('\\\\?\\D:/XY\\Z', keep_curdir=True)", r'\\?\D:/XY\Z')

def test_normpath_keep_pardir(self):
tester("ntpath.normpath('A//////././//.//B', keep_pardir=True)", r'A\B')
tester("ntpath.normpath('A/./B', keep_pardir=True)", r'A\B')
tester("ntpath.normpath('A/foo/../B', keep_pardir=True)", r'A\foo\..\B')
tester("ntpath.normpath('C:A//B', keep_pardir=True)", r'C:A\B')
tester("ntpath.normpath('D:A/./B', keep_pardir=True)", r'D:A\B')
tester("ntpath.normpath('e:A/foo/../B', keep_pardir=True)", r'e:A\foo\..\B')

tester("ntpath.normpath('C:///A//B', keep_pardir=True)", r'C:\A\B')
tester("ntpath.normpath('D:///A/./B', keep_pardir=True)", r'D:\A\B')
tester("ntpath.normpath('e:///A/foo/../B', keep_pardir=True)", r'e:\A\foo\..\B')

tester("ntpath.normpath('..', keep_pardir=True)", r'..')
tester("ntpath.normpath('.', keep_pardir=True)", r'.')
tester("ntpath.normpath('', keep_pardir=True)", r'.')
tester("ntpath.normpath('/', keep_pardir=True)", '\\')
tester("ntpath.normpath('c:/', keep_pardir=True)", 'c:\\')
tester("ntpath.normpath('/../.././..', keep_pardir=True)", '\\..\\..\\..')
tester("ntpath.normpath('c:/../../..', keep_pardir=True)", 'c:\\..\\..\\..')
tester("ntpath.normpath('../.././..', keep_pardir=True)", r'..\..\..')
tester("ntpath.normpath('K:../.././..', keep_pardir=True)", r'K:..\..\..')
tester("ntpath.normpath('C:////a/b', keep_pardir=True)", r'C:\a\b')
tester("ntpath.normpath('//machine/share//a/b', keep_pardir=True)", r'\\machine\share\a\b')

tester("ntpath.normpath('\\\\.\\NUL', keep_pardir=True)", r'\\.\NUL')
tester("ntpath.normpath('\\\\?\\D:/XY\\Z', keep_pardir=True)", r'\\?\D:/XY\Z')

def test_normpath_keep_curdir_and_pardir(self):
tester("ntpath.normpath("
"'A//////././//.//B', keep_curdir=True, keep_pardir=True)",
r'A\.\.\.\B')
tester("ntpath.normpath("
"'A/./B', keep_curdir=True, keep_pardir=True)",
r'A\.\B')
tester("ntpath.normpath("
"'A/foo/../B', keep_curdir=True, keep_pardir=True)",
r'A\foo\..\B')
tester("ntpath.normpath("
"'C:A//B', keep_curdir=True, keep_pardir=True)",
r'C:A\B')
tester("ntpath.normpath("
"'D:A/./B', keep_curdir=True, keep_pardir=True)",
r'D:A\.\B')
tester("ntpath.normpath("
"'e:A/foo/../B', keep_curdir=True, keep_pardir=True)",
r'e:A\foo\..\B')

tester("ntpath.normpath("
"'C:///A//B', keep_curdir=True, keep_pardir=True)",
r'C:\A\B')
tester("ntpath.normpath("
"'D:///A/./B', keep_curdir=True, keep_pardir=True)",
r'D:\A\.\B')
tester("ntpath.normpath("
"'e:///A/foo/../B', keep_curdir=True, keep_pardir=True)",
r'e:\A\foo\..\B')

tester("ntpath.normpath("
"'..', keep_curdir=True, keep_pardir=True)",
r'..')
tester("ntpath.normpath("
"'.', keep_curdir=True, keep_pardir=True)",
r'.')
tester("ntpath.normpath("
"'', keep_curdir=True, keep_pardir=True)",
r'.')
tester("ntpath.normpath("
"'/', keep_curdir=True, keep_pardir=True)",
'\\')
tester("ntpath.normpath("
"'c:/', keep_curdir=True, keep_pardir=True)",
'c:\\')
tester("ntpath.normpath("
"'/../.././..', keep_curdir=True, keep_pardir=True)",
'\\..\\..\\.\\..')
tester("ntpath.normpath("
"'c:/../../..', keep_curdir=True, keep_pardir=True)",
'c:\\..\\..\\..')
tester("ntpath.normpath("
"'../.././..', keep_curdir=True, keep_pardir=True)",
r'..\..\.\..')
tester("ntpath.normpath("
"'K:../.././..', keep_curdir=True, keep_pardir=True)",
r'K:..\..\.\..')
tester("ntpath.normpath("
"'C:////a/b', keep_curdir=True, keep_pardir=True)",
r'C:\a\b')
tester("ntpath.normpath("
"'//machine/share//a/b', keep_curdir=True, keep_pardir=True)",
r'\\machine\share\a\b')

tester("ntpath.normpath("
"'\\\\.\\NUL', keep_curdir=True, keep_pardir=True)",
r'\\.\NUL')
tester("ntpath.normpath("
"'\\\\?\\D:/XY\\Z', keep_curdir=True, keep_pardir=True)",
r'\\?\D:/XY\Z')

def test_realpath_curdir(self):
expected = ntpath.normpath(os.getcwd())
tester("ntpath.realpath('.')", expected)
Expand Down
103 changes: 103 additions & 0 deletions Lib/test/test_posixpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,109 @@ def test_normpath(self):
self.assertEqual(posixpath.normpath(b"///..//./foo/.//bar"),
b"/foo/bar")

def test_normpath_keep_curdir(self):
self.assertEqual(posixpath.normpath("", keep_curdir=True), ".")
self.assertEqual(posixpath.normpath("/", keep_curdir=True), "/")
self.assertEqual(posixpath.normpath("//", keep_curdir=True), "//")
self.assertEqual(posixpath.normpath("///", keep_curdir=True), "/")
self.assertEqual(posixpath.normpath(
"///foo/.//bar//", keep_curdir=True),
"/foo/./bar")
self.assertEqual(posixpath.normpath(
"///foo/.//bar//.//..//.//baz", keep_curdir=True),
"/foo/./bar/./baz")
self.assertEqual(posixpath.normpath(
"///..//./foo/.//bar", keep_curdir=True),
"/./foo/./bar")

self.assertEqual(posixpath.normpath(b"", keep_curdir=True), b".")
self.assertEqual(posixpath.normpath(b"/", keep_curdir=True), b"/")
self.assertEqual(posixpath.normpath(b"//", keep_curdir=True), b"//")
self.assertEqual(posixpath.normpath(b"///", keep_curdir=True), b"/")
self.assertEqual(posixpath.normpath(
b"///foo/.//bar//", keep_curdir=True),
b"/foo/./bar")
self.assertEqual(posixpath.normpath(
b"///foo/.//bar//.//..//.//baz", keep_curdir=True),
b"/foo/./bar/./baz")
self.assertEqual(posixpath.normpath(
b"///..//./foo/.//bar", keep_curdir=True),
b"/./foo/./bar")

def test_normpath_keep_pardir(self):
self.assertEqual(posixpath.normpath("", keep_pardir=True), ".")
self.assertEqual(posixpath.normpath("/", keep_pardir=True), "/")
self.assertEqual(posixpath.normpath("//", keep_pardir=True), "//")
self.assertEqual(posixpath.normpath("///", keep_pardir=True), "/")
self.assertEqual(posixpath.normpath(
"///foo/.//bar//", keep_pardir=True),
"/foo/bar")
self.assertEqual(posixpath.normpath(
"///foo/.//bar//.//..//.//baz", keep_pardir=True),
"/foo/bar/../baz")
self.assertEqual(posixpath.normpath(
"///..//./foo/.//bar", keep_pardir=True),
"/../foo/bar")

self.assertEqual(posixpath.normpath(b"", keep_pardir=True), b".")
self.assertEqual(posixpath.normpath(b"/", keep_pardir=True), b"/")
self.assertEqual(posixpath.normpath(b"//", keep_pardir=True), b"//")
self.assertEqual(posixpath.normpath(b"///", keep_pardir=True), b"/")
self.assertEqual(posixpath.normpath(
b"///foo/.//bar//", keep_pardir=True),
b"/foo/bar")
self.assertEqual(posixpath.normpath(
b"///foo/.//bar//.//..//.//baz", keep_pardir=True),
b"/foo/bar/../baz")
self.assertEqual(posixpath.normpath(
b"///..//./foo/.//bar", keep_pardir=True),
b"/../foo/bar")

def test_normpath_keep_curdir_and_pardir(self):
self.assertEqual(posixpath.normpath(
"", keep_curdir=True, keep_pardir=True),
".")
self.assertEqual(posixpath.normpath(
"/", keep_curdir=True, keep_pardir=True),
"/")
self.assertEqual(posixpath.normpath(
"//", keep_curdir=True, keep_pardir=True),
"//")
self.assertEqual(posixpath.normpath(
"///", keep_curdir=True, keep_pardir=True),
"/")
self.assertEqual(posixpath.normpath(
"///foo/.//bar//", keep_curdir=True, keep_pardir=True),
"/foo/./bar")
self.assertEqual(posixpath.normpath(
"///foo/.//bar//.//..//.//baz", keep_curdir=True, keep_pardir=True),
"/foo/./bar/./.././baz")
self.assertEqual(posixpath.normpath(
"///..//./foo/.//bar", keep_curdir=True, keep_pardir=True),
"/.././foo/./bar")

self.assertEqual(posixpath.normpath(
b"", keep_curdir=True, keep_pardir=True),
b".")
self.assertEqual(posixpath.normpath(
b"/", keep_curdir=True, keep_pardir=True),
b"/")
self.assertEqual(posixpath.normpath(
b"//", keep_curdir=True, keep_pardir=True),
b"//")
self.assertEqual(posixpath.normpath(
b"///", keep_curdir=True, keep_pardir=True),
b"/")
self.assertEqual(posixpath.normpath(
b"///foo/.//bar//", keep_curdir=True, keep_pardir=True),
b"/foo/./bar")
self.assertEqual(posixpath.normpath(
b"///foo/.//bar//.//..//.//baz", keep_curdir=True, keep_pardir=True),
b"/foo/./bar/./.././baz")
self.assertEqual(posixpath.normpath(
b"///..//./foo/.//bar", keep_curdir=True, keep_pardir=True),
b"/.././foo/./bar")

@skip_if_ABSTFN_contains_backslash
def test_realpath_curdir(self):
self.assertEqual(realpath('.'), os.getcwd())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:func:`os.path.normpath` now accepts *keep_curdir* and *keep_pardir*
keyword-only arguments. When set to ``True``, references to the current or
parent directory (``.`` or ``..``) are retained respectively.