Skip to content

GH-125866: Deprecate nturl2path module #131432

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 8 commits into from
Mar 19, 2025
Merged
Show file tree
Hide file tree
Changes from 6 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
5 changes: 5 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1135,6 +1135,11 @@ Deprecated
or *sequence* as keyword arguments is now deprecated.
(Contributed by Kirill Podoprigora in :gh:`121676`.)

* :mod:`!nturl2path`: This module is now deprecated. Call
:func:`urllib.request.url2pathname` and :func:`~urllib.request.pathname2url`
instead.
(Contributed by Barney Gale in :gh:`125866`.)

* :mod:`os`:
:term:`Soft deprecate <soft deprecated>` :func:`os.popen` and
:func:`os.spawn* <os.spawnl>` functions. They should no longer be used to
Expand Down
7 changes: 6 additions & 1 deletion Lib/nturl2path.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
This module only exists to provide OS-specific code
for urllib.requests, thus do not use directly.
"""
# Testing is done through test_urllib.
# Testing is done through test_nturl2path.

import warnings


warnings._deprecated(__name__, remove=(3, 19))

def url2pathname(url):
"""OS-specific conversion from a relative URL of the 'file' scheme
Expand Down
107 changes: 107 additions & 0 deletions Lib/test/test_nturl2path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import unittest

from test.support import warnings_helper


nturl2path = warnings_helper.import_deprecated("nturl2path")


class NTURL2PathTest(unittest.TestCase):
"""Test pathname2url() and url2pathname()"""

def test_basic(self):
# Make sure simple tests pass
expected_path = r"parts\of\a\path"
expected_url = "parts/of/a/path"
result = nturl2path.pathname2url(expected_path)
self.assertEqual(expected_url, result,
"pathname2url() failed; %s != %s" %
(result, expected_url))
result = nturl2path.url2pathname(expected_url)
self.assertEqual(expected_path, result,
"url2pathame() failed; %s != %s" %
(result, expected_path))

def test_pathname2url(self):
# Test special prefixes are correctly handled in pathname2url()
fn = nturl2path.pathname2url
self.assertEqual(fn('\\\\?\\C:\\dir'), '///C:/dir')
self.assertEqual(fn('\\\\?\\unc\\server\\share\\dir'), '//server/share/dir')
self.assertEqual(fn("C:"), '///C:')
self.assertEqual(fn("C:\\"), '///C:/')
self.assertEqual(fn('c:\\a\\b.c'), '///c:/a/b.c')
self.assertEqual(fn('C:\\a\\b.c'), '///C:/a/b.c')
self.assertEqual(fn('C:\\a\\b.c\\'), '///C:/a/b.c/')
self.assertEqual(fn('C:\\a\\\\b.c'), '///C:/a//b.c')
self.assertEqual(fn('C:\\a\\b%#c'), '///C:/a/b%25%23c')
self.assertEqual(fn('C:\\a\\b\xe9'), '///C:/a/b%C3%A9')
self.assertEqual(fn('C:\\foo\\bar\\spam.foo'), "///C:/foo/bar/spam.foo")
# NTFS alternate data streams
self.assertEqual(fn('C:\\foo:bar'), '///C:/foo%3Abar')
self.assertEqual(fn('foo:bar'), 'foo%3Abar')
# No drive letter
self.assertEqual(fn("\\folder\\test\\"), '///folder/test/')
self.assertEqual(fn("\\\\folder\\test\\"), '//folder/test/')
self.assertEqual(fn("\\\\\\folder\\test\\"), '///folder/test/')
self.assertEqual(fn('\\\\some\\share\\'), '//some/share/')
self.assertEqual(fn('\\\\some\\share\\a\\b.c'), '//some/share/a/b.c')
self.assertEqual(fn('\\\\some\\share\\a\\b%#c\xe9'), '//some/share/a/b%25%23c%C3%A9')
# Alternate path separator
self.assertEqual(fn('C:/a/b.c'), '///C:/a/b.c')
self.assertEqual(fn('//some/share/a/b.c'), '//some/share/a/b.c')
self.assertEqual(fn('//?/C:/dir'), '///C:/dir')
self.assertEqual(fn('//?/unc/server/share/dir'), '//server/share/dir')
# Round-tripping
urls = ['///C:',
'///folder/test/',
'///C:/foo/bar/spam.foo']
for url in urls:
self.assertEqual(fn(nturl2path.url2pathname(url)), url)

def test_url2pathname(self):
fn = nturl2path.url2pathname
self.assertEqual(fn('/'), '\\')
self.assertEqual(fn('/C:/'), 'C:\\')
self.assertEqual(fn("///C|"), 'C:')
self.assertEqual(fn("///C:"), 'C:')
self.assertEqual(fn('///C:/'), 'C:\\')
self.assertEqual(fn('/C|//'), 'C:\\\\')
self.assertEqual(fn('///C|/path'), 'C:\\path')
# No DOS drive
self.assertEqual(fn("///C/test/"), '\\C\\test\\')
self.assertEqual(fn("////C/test/"), '\\\\C\\test\\')
# DOS drive paths
self.assertEqual(fn('c:/path/to/file'), 'c:\\path\\to\\file')
self.assertEqual(fn('C:/path/to/file'), 'C:\\path\\to\\file')
self.assertEqual(fn('C:/path/to/file/'), 'C:\\path\\to\\file\\')
self.assertEqual(fn('C:/path/to//file'), 'C:\\path\\to\\\\file')
self.assertEqual(fn('C|/path/to/file'), 'C:\\path\\to\\file')
self.assertEqual(fn('/C|/path/to/file'), 'C:\\path\\to\\file')
self.assertEqual(fn('///C|/path/to/file'), 'C:\\path\\to\\file')
self.assertEqual(fn("///C|/foo/bar/spam.foo"), 'C:\\foo\\bar\\spam.foo')
# Colons in URI
self.assertEqual(fn('///\u00e8|/'), '\u00e8:\\')
self.assertEqual(fn('//host/share/spam.txt:eggs'), '\\\\host\\share\\spam.txt:eggs')
self.assertEqual(fn('///c:/spam.txt:eggs'), 'c:\\spam.txt:eggs')
# UNC paths
self.assertEqual(fn('//server/path/to/file'), '\\\\server\\path\\to\\file')
self.assertEqual(fn('////server/path/to/file'), '\\\\server\\path\\to\\file')
self.assertEqual(fn('/////server/path/to/file'), '\\\\server\\path\\to\\file')
# Localhost paths
self.assertEqual(fn('//localhost/C:/path/to/file'), 'C:\\path\\to\\file')
self.assertEqual(fn('//localhost/C|/path/to/file'), 'C:\\path\\to\\file')
self.assertEqual(fn('//localhost/path/to/file'), '\\path\\to\\file')
self.assertEqual(fn('//localhost//server/path/to/file'), '\\\\server\\path\\to\\file')
# Percent-encoded forward slashes are preserved for backwards compatibility
self.assertEqual(fn('C:/foo%2fbar'), 'C:\\foo/bar')
self.assertEqual(fn('//server/share/foo%2fbar'), '\\\\server\\share\\foo/bar')
# Round-tripping
paths = ['C:',
r'\C\test\\',
r'C:\foo\bar\spam.foo']
for path in paths:
self.assertEqual(fn(nturl2path.pathname2url(path)), path)


if __name__ == '__main__':
unittest.main()
4 changes: 0 additions & 4 deletions Lib/test/test_urllib2.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,6 @@ def test___all__(self):
context = {}
exec('from urllib.%s import *' % module, context)
del context['__builtins__']
if module == 'request' and os.name == 'nt':
u, p = context.pop('url2pathname'), context.pop('pathname2url')
self.assertEqual(u.__module__, 'nturl2path')
self.assertEqual(p.__module__, 'nturl2path')
for k, v in context.items():
self.assertEqual(v.__module__, 'urllib.%s' % module,
"%r is exposed in 'urllib.%s' but defined in %r" %
Expand Down
87 changes: 57 additions & 30 deletions Lib/urllib/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -1646,36 +1646,63 @@ def data_open(self, req):

# Code move from the old urllib module

# Helper for non-unix systems
if os.name == 'nt':
from nturl2path import url2pathname, pathname2url
else:
def url2pathname(pathname):
"""OS-specific conversion from a relative URL of the 'file' scheme
to a file system path; not recommended for general use."""
if pathname[:3] == '///':
# URL has an empty authority section, so the path begins on the
# third character.
pathname = pathname[2:]
elif pathname[:12] == '//localhost/':
# Skip past 'localhost' authority.
pathname = pathname[11:]
encoding = sys.getfilesystemencoding()
errors = sys.getfilesystemencodeerrors()
return unquote(pathname, encoding=encoding, errors=errors)

def pathname2url(pathname):
"""OS-specific conversion from a file system path to a relative URL
of the 'file' scheme; not recommended for general use."""
if pathname[:1] == '/':
# Add explicitly empty authority to absolute path. If the path
# starts with exactly one slash then this change is mostly
# cosmetic, but if it begins with two or more slashes then this
# avoids interpreting the path as a URL authority.
pathname = '//' + pathname
encoding = sys.getfilesystemencoding()
errors = sys.getfilesystemencodeerrors()
return quote(pathname, encoding=encoding, errors=errors)
def url2pathname(url):
"""OS-specific conversion from a relative URL of the 'file' scheme
to a file system path; not recommended for general use."""
if url[:3] == '///':
# Empty authority section, so the path begins on the third character.
url = url[2:]
elif url[:12] == '//localhost/':
# Skip past 'localhost' authority.
url = url[11:]

if os.name == 'nt':
if url[:3] == '///':
# Skip past extra slash before UNC drive in URL path.
url = url[1:]
else:
if url[:1] == '/' and url[2:3] in (':', '|'):
# Skip past extra slash before DOS drive in URL path.
url = url[1:]
if url[1:2] == '|':
# Older URLs use a pipe after a drive letter
url = url[:1] + ':' + url[2:]
url = url.replace('/', '\\')
encoding = sys.getfilesystemencoding()
errors = sys.getfilesystemencodeerrors()
return unquote(url, encoding=encoding, errors=errors)


def pathname2url(pathname):
"""OS-specific conversion from a file system path to a relative URL
of the 'file' scheme; not recommended for general use."""
if os.name == 'nt':
pathname = pathname.replace('\\', '/')
encoding = sys.getfilesystemencoding()
errors = sys.getfilesystemencodeerrors()
prefix = ''
drive, root, tail = os.path.splitroot(pathname)
if drive:
# First, clean up some special forms. We are going to sacrifice the
# additional information anyway
if drive[:4] == '//?/':
drive = drive[4:]
if drive[:4].upper() == 'UNC/':
drive = '//' + drive[4:]
if drive[1:] == ':':
# DOS drive specified. Add three slashes to the start, producing
# an authority section with a zero-length authority, and a path
# section starting with a single slash.
prefix += '///'
drive = quote(drive, encoding=encoding, errors=errors, safe='/:')
elif root:
# Add explicitly empty authority to absolute path. If the path
# starts with exactly one slash then this change is mostly
# cosmetic, but if it begins with two or more slashes then this
# avoids interpreting the path as a URL authority.
prefix += '//'
tail = quote(tail, encoding=encoding, errors=errors)
return prefix + drive + root + tail


# Utility functions
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Deprecate the :mod:`!nturl2path` module. Call
:func:`urllib.request.url2pathname` and :func:`~urllib.request.pathname2url`
instead.
Loading