diff --git a/Lib/ntpath.py b/Lib/ntpath.py index b3c23f0abc2d889..d30b144a2e35d33 100644 --- a/Lib/ntpath.py +++ b/Lib/ntpath.py @@ -516,6 +516,26 @@ def normpath(path): from nt import _getfullpathname except ImportError: # not running on Windows - mock up something sensible + def _strip_dots_and_spaces(path): + # As the Windows path normalization does. + if isinstance(path, bytes): + sep = b'\\' + dot = b'.' + dots_and_spaces = b'. ' + else: + sep = '\\' + dot = '.' + dots_and_spaces = '. ' + drive, root, tail = splitroot(path) + if not tail: + return path + comps = tail.split(sep) + for i, comp in enumerate(comps[:-1]): + if len(comp) > 1 and comp.endswith(dot) and not comp.endswith(dot*2): + comps[i] = comp[:-1] + comps[-1] = comps[-1].rstrip(dots_and_spaces) + return drive + root + sep.join(comps) + def abspath(path): """Return the absolute version of a path.""" path = os.fspath(path) @@ -525,7 +545,7 @@ def abspath(path): else: cwd = os.getcwd() path = join(cwd, path) - return normpath(path) + return _strip_dots_and_spaces(normpath(path)) else: # use native Windows method on Windows def abspath(path): diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py index 936332bf94ffe77..14bdb97b5ab7f65 100644 --- a/Lib/test/test_ntpath.py +++ b/Lib/test/test_ntpath.py @@ -1210,6 +1210,35 @@ def test_expanduser(self): + def test_abspath_dots_and_spaces(self): + # gh-85681: trailing dots and spaces are stripped from the last + # component, and a single trailing dot from other components. + tester('ntpath.abspath("C:/spam. . .")', "C:\\spam") + tester('ntpath.abspath("C:/spam.")', "C:\\spam") + tester('ntpath.abspath("C:/spam ")', "C:\\spam") + tester('ntpath.abspath("C:/spam..")', "C:\\spam") + tester('ntpath.abspath("C:/.spam")', "C:\\.spam") + tester('ntpath.abspath("C:/spam./eggs")', "C:\\spam\\eggs") + tester('ntpath.abspath("C:/spam.b./eggs")', "C:\\spam.b\\eggs") + tester('ntpath.abspath("C:/spam ./eggs")', "C:\\spam \\eggs") + # A trailing dot preceded by a dot is not stripped. + tester('ntpath.abspath("C:/spam../eggs")', "C:\\spam..\\eggs") + tester('ntpath.abspath("C:/spam.../eggs")', "C:\\spam...\\eggs") + # Trailing dots and spaces are stripped even in extended paths. + tester('ntpath.abspath("\\\\?\\C:/spam. . .")', "\\\\?\\C:\\spam") + tester('ntpath.abspath("\\\\.\\C:/spam. . .")', "\\\\.\\C:\\spam") + tester('ntpath.abspath("//server/share/spam. ")', + "\\\\server\\share\\spam") + + def test_relpath_dots_and_spaces(self): + # gh-85681: relpath() is based on abspath(). + tester('ntpath.relpath("foo ", "foo")', ".") + tester('ntpath.relpath("foo.", "foo")', ".") + tester('ntpath.relpath("foo/bar ", "foo")', "bar") + tester('ntpath.relpath("foo/bar.", "foo")', "bar") + # "foo.." denotes "foo" as the last component, but not as other. + tester('ntpath.relpath("foo../bar", "foo..")', "..\\foo..\\bar") + @unittest.skipUnless(nt, "abspath requires 'nt' module") def test_abspath(self): tester('ntpath.abspath("C:\\")', "C:\\") diff --git a/Misc/NEWS.d/next/Library/2026-08-08-23-30-00.gh-issue-85681.ntabs.rst b/Misc/NEWS.d/next/Library/2026-08-08-23-30-00.gh-issue-85681.ntabs.rst new file mode 100644 index 000000000000000..ffd3f0a885c938c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-08-23-30-00.gh-issue-85681.ntabs.rst @@ -0,0 +1,3 @@ +:func:`ntpath.abspath` on non-Windows platforms now strips trailing dots and +spaces like the Windows path normalization does. This makes +:func:`ntpath.relpath` and other functions based on it platform independent.