diff --git a/Doc/conf.py b/Doc/conf.py
index 8a14646801ebac..1d03da1dc92935 100644
--- a/Doc/conf.py
+++ b/Doc/conf.py
@@ -200,7 +200,10 @@
# Undocumented modules that users shouldn't have to worry about
# (implementation details of `os.path`):
('py:mod', 'ntpath'),
+ ('py:mod', 'ntpath.pure'),
('py:mod', 'posixpath'),
+ ('py:mod', 'posixpath.pure'),
+ ('py:mod', 'os.path.pure'),
]
# Temporary undocumented names.
diff --git a/Doc/library/os.path.rst b/Doc/library/os.path.rst
index b582321515db56..9a36e13b004c2a 100644
--- a/Doc/library/os.path.rst
+++ b/Doc/library/os.path.rst
@@ -4,8 +4,8 @@
.. module:: os.path
:synopsis: Operations on pathnames.
-**Source code:** :source:`Lib/genericpath.py`, :source:`Lib/posixpath.py` (for POSIX) and
-:source:`Lib/ntpath.py` (for Windows).
+**Source code:** :source:`Lib/genericpath.py`, :source:`Lib/posixpath/` (for POSIX) and
+:source:`Lib/ntpath/` (for Windows).
.. index:: single: path; operations
@@ -42,8 +42,8 @@ the :mod:`glob` module.)
a path that is *always* in one of the different formats. They all have the
same interface:
- * :mod:`posixpath` for UNIX-style paths
- * :mod:`ntpath` for Windows paths
+ * :mod:`posixpath.pure` for UNIX-style paths
+ * :mod:`ntpath.pure` for Windows paths
.. versionchanged:: 3.8
diff --git a/Lib/ntpath.py b/Lib/ntpath/__init__.py
similarity index 100%
rename from Lib/ntpath.py
rename to Lib/ntpath/__init__.py
diff --git a/Lib/ntpath/pure.py b/Lib/ntpath/pure.py
new file mode 100644
index 00000000000000..9b8d339fd5523a
--- /dev/null
+++ b/Lib/ntpath/pure.py
@@ -0,0 +1,22 @@
+# Module 'ntpath.pure' -- pure operations on WinNT/Win95 pathnames
+"""Pure pathname manipulations, WindowsNT/95 version."""
+import os
+import ntpath
+from ntpath import (altsep, basename, commonpath, commonprefix, curdir,
+ dirname, extsep, isabs, isreserved, join, normcase,
+ normpath, pardir, pathsep, sep, split, splitdrive,
+ splitext, splitroot)
+
+__all__ = ["altsep", "basename", "commonpath", "commonprefix", "curdir",
+ "dirname", "extsep", "isabs", "isreserved", "join", "normcase",
+ "normpath", "pardir", "pathsep", "relpath", "sep", "split",
+ "splitdrive", "splitext", "splitroot"]
+
+def relpath(path, start):
+ """Return a relative version of a path"""
+ path = os.fspath(path)
+ start = os.fspath(start)
+ if not isabs(path) or not isabs(start):
+ raise ValueError("paths are not absolute")
+
+ return ntpath.relpath(path, start)
diff --git a/Lib/posixpath.py b/Lib/posixpath/__init__.py
similarity index 99%
rename from Lib/posixpath.py
rename to Lib/posixpath/__init__.py
index fccca4e066b76f..3b062c7e5c59bf 100644
--- a/Lib/posixpath.py
+++ b/Lib/posixpath/__init__.py
@@ -5,9 +5,6 @@
module on Posix systems; on other systems (e.g. Windows),
os.path provides the same operations in a manner specific to that
platform, and is an alias to another module (e.g. ntpath).
-
-Some of this can actually be useful on non-Posix systems too, e.g.
-for manipulation of the pathname component of URLs.
"""
# Strings representing various path-related bits and pieces.
diff --git a/Lib/posixpath/pure.py b/Lib/posixpath/pure.py
new file mode 100644
index 00000000000000..72bfa3695c907f
--- /dev/null
+++ b/Lib/posixpath/pure.py
@@ -0,0 +1,25 @@
+"""Pure operations on Posix pathnames.
+
+This can actually be useful on non-Posix systems too, e.g.
+for manipulation of the pathname component of URLs.
+"""
+import os
+import posixpath
+from posixpath import (altsep, basename, commonpath, commonprefix, curdir,
+ dirname, extsep, isabs, join, normcase, normpath,
+ pardir, pathsep, sep, split, splitdrive, splitext,
+ splitroot)
+
+__all__ = ["altsep", "basename", "commonpath", "commonprefix", "curdir",
+ "dirname", "extsep", "isabs", "join", "normcase", "normpath",
+ "pardir", "pathsep", "relpath", "sep", "split", "splitdrive",
+ "splitext", "splitroot"]
+
+def relpath(path, start):
+ """Return a relative version of a path"""
+ path = os.fspath(path)
+ start = os.fspath(start)
+ if not isabs(path) or not isabs(start):
+ raise ValueError("paths are not absolute")
+
+ return posixpath.relpath(path, start)
diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py
index 64cbfaaaaa0690..11bc91ca5bef55 100644
--- a/Lib/test/test_ntpath.py
+++ b/Lib/test/test_ntpath.py
@@ -1,5 +1,6 @@
import inspect
import ntpath
+import ntpath.pure
import os
import string
import subprocess
@@ -863,6 +864,17 @@ def test_relpath(self):
tester('ntpath.relpath("/a/b", "/a/b")', '.')
tester('ntpath.relpath("c:/foo", "C:/FOO")', '.')
+ def test_pure_relpath(self):
+ self.assertRaises(ValueError, ntpath.pure.relpath, 'foo', 'bar')
+ self.assertRaises(ValueError, ntpath.pure.relpath, 'foo', 'C:/bar')
+ self.assertRaises(ValueError, ntpath.pure.relpath, 'C:/foo', 'bar')
+ tester('ntpath.pure.relpath("C:/foo", "C:/bar")', r'..\foo')
+
+ # test bytes
+ self.assertRaises(ValueError, ntpath.pure.relpath, b'foo', b'bar')
+ self.assertRaises(ValueError, ntpath.pure.relpath, b'foo', b'C:/bar')
+ self.assertRaises(ValueError, ntpath.pure.relpath, b'C:/foo', b'bar')
+
def test_commonpath(self):
def check(paths, expected):
tester(('ntpath.commonpath(%r)' % paths).replace('\\\\', '\\'),
diff --git a/Lib/test/test_posixpath.py b/Lib/test/test_posixpath.py
index 57a24e9c70d5e5..74c4a12a94d408 100644
--- a/Lib/test/test_posixpath.py
+++ b/Lib/test/test_posixpath.py
@@ -1,6 +1,7 @@
import inspect
import os
import posixpath
+import posixpath.pure
import sys
import unittest
from posixpath import realpath, abspath, dirname, basename
@@ -746,6 +747,18 @@ def test_relpath_bytes(self):
finally:
os.getcwdb = real_getcwdb
+ def test_pure_relpath(self):
+ self.assertRaises(ValueError, posixpath.pure.relpath, 'foo', 'bar')
+ self.assertRaises(ValueError, posixpath.pure.relpath, 'foo', '/bar')
+ self.assertRaises(ValueError, posixpath.pure.relpath, '/foo', 'bar')
+ self.assertEqual(posixpath.pure.relpath('/foo', '/bar'), '../foo')
+
+ def test_pure_relpath_bytes(self):
+ self.assertRaises(ValueError, posixpath.pure.relpath, b'foo', b'bar')
+ self.assertRaises(ValueError, posixpath.pure.relpath, b'foo', b'/bar')
+ self.assertRaises(ValueError, posixpath.pure.relpath, b'/foo', b'bar')
+ self.assertEqual(posixpath.pure.relpath(b'/foo', b'/bar'), b'../foo')
+
def test_commonpath(self):
def check(paths, expected):
self.assertEqual(posixpath.commonpath(paths), expected)
diff --git a/Makefile.pre.in b/Makefile.pre.in
index 22dba279faa935..7d724483c54278 100644
--- a/Makefile.pre.in
+++ b/Makefile.pre.in
@@ -1468,8 +1468,10 @@ FROZEN_FILES_IN = \
Lib/_collections_abc.py \
Lib/_sitebuiltins.py \
Lib/genericpath.py \
- Lib/ntpath.py \
- Lib/posixpath.py \
+ Lib/ntpath/__init__.py \
+ Lib/ntpath/pure.py \
+ Lib/posixpath/__init__.py \
+ Lib/posixpath/pure.py \
Lib/os.py \
Lib/site.py \
Lib/stat.py \
@@ -1494,7 +1496,9 @@ FROZEN_FILES_OUT = \
Python/frozen_modules/_sitebuiltins.h \
Python/frozen_modules/genericpath.h \
Python/frozen_modules/ntpath.h \
+ Python/frozen_modules/ntpath.pure.h \
Python/frozen_modules/posixpath.h \
+ Python/frozen_modules/posixpath.pure.h \
Python/frozen_modules/os.h \
Python/frozen_modules/site.h \
Python/frozen_modules/stat.h \
@@ -1549,11 +1553,17 @@ Python/frozen_modules/_sitebuiltins.h: Lib/_sitebuiltins.py $(FREEZE_MODULE_DEPS
Python/frozen_modules/genericpath.h: Lib/genericpath.py $(FREEZE_MODULE_DEPS)
$(FREEZE_MODULE) genericpath $(srcdir)/Lib/genericpath.py Python/frozen_modules/genericpath.h
-Python/frozen_modules/ntpath.h: Lib/ntpath.py $(FREEZE_MODULE_DEPS)
- $(FREEZE_MODULE) ntpath $(srcdir)/Lib/ntpath.py Python/frozen_modules/ntpath.h
+Python/frozen_modules/ntpath.h: Lib/ntpath/__init__.py $(FREEZE_MODULE_DEPS)
+ $(FREEZE_MODULE) ntpath $(srcdir)/Lib/ntpath/__init__.py Python/frozen_modules/ntpath.h
-Python/frozen_modules/posixpath.h: Lib/posixpath.py $(FREEZE_MODULE_DEPS)
- $(FREEZE_MODULE) posixpath $(srcdir)/Lib/posixpath.py Python/frozen_modules/posixpath.h
+Python/frozen_modules/ntpath.pure.h: Lib/ntpath/pure.py $(FREEZE_MODULE_DEPS)
+ $(FREEZE_MODULE) ntpath.pure $(srcdir)/Lib/ntpath/pure.py Python/frozen_modules/ntpath.pure.h
+
+Python/frozen_modules/posixpath.h: Lib/posixpath/__init__.py $(FREEZE_MODULE_DEPS)
+ $(FREEZE_MODULE) posixpath $(srcdir)/Lib/posixpath/__init__.py Python/frozen_modules/posixpath.h
+
+Python/frozen_modules/posixpath.pure.h: Lib/posixpath/pure.py $(FREEZE_MODULE_DEPS)
+ $(FREEZE_MODULE) posixpath.pure $(srcdir)/Lib/posixpath/pure.py Python/frozen_modules/posixpath.pure.h
Python/frozen_modules/os.h: Lib/os.py $(FREEZE_MODULE_DEPS)
$(FREEZE_MODULE) os $(srcdir)/Lib/os.py Python/frozen_modules/os.h
diff --git a/Misc/NEWS.d/next/Library/2024-06-13-10-12-04.gh-issue-119671.-D116h.rst b/Misc/NEWS.d/next/Library/2024-06-13-10-12-04.gh-issue-119671.-D116h.rst
new file mode 100644
index 00000000000000..5b53368bbb242d
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-06-13-10-12-04.gh-issue-119671.-D116h.rst
@@ -0,0 +1 @@
+Add the :mod:`ntpath.pure`, :mod:`os.path.pure`, :mod:`posixpath.pure` modules for low-level path manipulation.
diff --git a/PCbuild/_freeze_module.vcxproj b/PCbuild/_freeze_module.vcxproj
index e5e18de60ec349..1e0c01700e2172 100644
--- a/PCbuild/_freeze_module.vcxproj
+++ b/PCbuild/_freeze_module.vcxproj
@@ -318,16 +318,26 @@
$(IntDir)genericpath.g.h
$(GeneratedFrozenModulesDir)Python\frozen_modules\genericpath.h
-
+
ntpath
$(IntDir)ntpath.g.h
$(GeneratedFrozenModulesDir)Python\frozen_modules\ntpath.h
-
+
+ ntpath.pure
+ $(IntDir)ntpath.pure.g.h
+ $(GeneratedFrozenModulesDir)Python\frozen_modules\ntpath.pure.h
+
+
posixpath
$(IntDir)posixpath.g.h
$(GeneratedFrozenModulesDir)Python\frozen_modules\posixpath.h
+
+ posixpath.pure
+ $(IntDir)posixpath.pure.g.h
+ $(GeneratedFrozenModulesDir)Python\frozen_modules\posixpath.pure.h
+
os
$(IntDir)os.g.h
@@ -410,7 +420,9 @@
+
+
diff --git a/PCbuild/_freeze_module.vcxproj.filters b/PCbuild/_freeze_module.vcxproj.filters
index 9630f54ae4ea29..6fdd19d3748bc9 100644
--- a/PCbuild/_freeze_module.vcxproj.filters
+++ b/PCbuild/_freeze_module.vcxproj.filters
@@ -523,10 +523,16 @@
Python Files
-
+
Python Files
-
+
+ Python Files
+
+
+ Python Files
+
+
Python Files
diff --git a/Python/frozen.c b/Python/frozen.c
index 627f2ff9413562..a356088a915f5f 100644
--- a/Python/frozen.c
+++ b/Python/frozen.c
@@ -51,7 +51,9 @@
#include "frozen_modules/_sitebuiltins.h"
#include "frozen_modules/genericpath.h"
#include "frozen_modules/ntpath.h"
+#include "frozen_modules/ntpath.pure.h"
#include "frozen_modules/posixpath.h"
+#include "frozen_modules/posixpath.pure.h"
#include "frozen_modules/os.h"
#include "frozen_modules/site.h"
#include "frozen_modules/stat.h"
@@ -82,9 +84,12 @@ static const struct _frozen stdlib_modules[] = {
{"_collections_abc", _Py_M___collections_abc, (int)sizeof(_Py_M___collections_abc), false},
{"_sitebuiltins", _Py_M___sitebuiltins, (int)sizeof(_Py_M___sitebuiltins), false},
{"genericpath", _Py_M__genericpath, (int)sizeof(_Py_M__genericpath), false},
- {"ntpath", _Py_M__ntpath, (int)sizeof(_Py_M__ntpath), false},
- {"posixpath", _Py_M__posixpath, (int)sizeof(_Py_M__posixpath), false},
- {"os.path", _Py_M__posixpath, (int)sizeof(_Py_M__posixpath), false},
+ {"ntpath", _Py_M__ntpath, (int)sizeof(_Py_M__ntpath), true},
+ {"ntpath.pure", _Py_M__ntpath_pure, (int)sizeof(_Py_M__ntpath_pure), false},
+ {"posixpath", _Py_M__posixpath, (int)sizeof(_Py_M__posixpath), true},
+ {"posixpath.pure", _Py_M__posixpath_pure, (int)sizeof(_Py_M__posixpath_pure), false},
+ {"os.path", _Py_M__posixpath, (int)sizeof(_Py_M__posixpath), true},
+ {"os.path.pure", _Py_M__posixpath_pure, (int)sizeof(_Py_M__posixpath_pure), false},
{"os", _Py_M__os, (int)sizeof(_Py_M__os), false},
{"site", _Py_M__site, (int)sizeof(_Py_M__site), false},
{"stat", _Py_M__stat, (int)sizeof(_Py_M__stat), false},
@@ -116,7 +121,8 @@ const struct _frozen *_PyImport_FrozenTest = test_modules;
static const struct _module_alias aliases[] = {
{"_frozen_importlib", "importlib._bootstrap"},
{"_frozen_importlib_external", "importlib._bootstrap_external"},
- {"os.path", "posixpath"},
+ {"os.path", "',
+ 'ntpath.pure',
+ '',
+ 'posixpath.pure',
+ # We must explicitly mark os.path and os.path.pure as frozen modules
+ # even though they will never be imported.
+ f'<{OS_PATH}> : os.path',
+ f'{OS_PATH}.pure : os.path.pure',
'os',
'site',
'stat',