Skip to content

Commit 8abfaba

Browse files
authored
GH-125866: Deprecate nturl2path module (#131432)
Deprecate the `nturl2path` module. Its functionality is merged into `urllib.request`. Add `tests.test_nturl2path` to exercise `nturl2path`, as it's no longer covered by `test_urllib`.
1 parent 8a33034 commit 8abfaba

File tree

6 files changed

+180
-35
lines changed

6 files changed

+180
-35
lines changed

Doc/whatsnew/3.14.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,6 +1148,11 @@ Deprecated
11481148
or *sequence* as keyword arguments is now deprecated.
11491149
(Contributed by Kirill Podoprigora in :gh:`121676`.)
11501150

1151+
* :mod:`!nturl2path`: This module is now deprecated. Call
1152+
:func:`urllib.request.url2pathname` and :func:`~urllib.request.pathname2url`
1153+
instead.
1154+
(Contributed by Barney Gale in :gh:`125866`.)
1155+
11511156
* :mod:`os`:
11521157
:term:`Soft deprecate <soft deprecated>` :func:`os.popen` and
11531158
:func:`os.spawn* <os.spawnl>` functions. They should no longer be used to

Lib/nturl2path.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,15 @@
33
This module only exists to provide OS-specific code
44
for urllib.requests, thus do not use directly.
55
"""
6-
# Testing is done through test_urllib.
6+
# Testing is done through test_nturl2path.
7+
8+
import warnings
9+
10+
11+
warnings._deprecated(
12+
__name__,
13+
message=f"{warnings._DEPRECATED_MSG}; use 'urllib.request' instead",
14+
remove=(3, 19))
715

816
def url2pathname(url):
917
"""OS-specific conversion from a relative URL of the 'file' scheme

Lib/test/test_nturl2path.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import unittest
2+
3+
from test.support import warnings_helper
4+
5+
6+
nturl2path = warnings_helper.import_deprecated("nturl2path")
7+
8+
9+
class NTURL2PathTest(unittest.TestCase):
10+
"""Test pathname2url() and url2pathname()"""
11+
12+
def test_basic(self):
13+
# Make sure simple tests pass
14+
expected_path = r"parts\of\a\path"
15+
expected_url = "parts/of/a/path"
16+
result = nturl2path.pathname2url(expected_path)
17+
self.assertEqual(expected_url, result,
18+
"pathname2url() failed; %s != %s" %
19+
(result, expected_url))
20+
result = nturl2path.url2pathname(expected_url)
21+
self.assertEqual(expected_path, result,
22+
"url2pathame() failed; %s != %s" %
23+
(result, expected_path))
24+
25+
def test_pathname2url(self):
26+
# Test special prefixes are correctly handled in pathname2url()
27+
fn = nturl2path.pathname2url
28+
self.assertEqual(fn('\\\\?\\C:\\dir'), '///C:/dir')
29+
self.assertEqual(fn('\\\\?\\unc\\server\\share\\dir'), '//server/share/dir')
30+
self.assertEqual(fn("C:"), '///C:')
31+
self.assertEqual(fn("C:\\"), '///C:/')
32+
self.assertEqual(fn('c:\\a\\b.c'), '///c:/a/b.c')
33+
self.assertEqual(fn('C:\\a\\b.c'), '///C:/a/b.c')
34+
self.assertEqual(fn('C:\\a\\b.c\\'), '///C:/a/b.c/')
35+
self.assertEqual(fn('C:\\a\\\\b.c'), '///C:/a//b.c')
36+
self.assertEqual(fn('C:\\a\\b%#c'), '///C:/a/b%25%23c')
37+
self.assertEqual(fn('C:\\a\\b\xe9'), '///C:/a/b%C3%A9')
38+
self.assertEqual(fn('C:\\foo\\bar\\spam.foo'), "///C:/foo/bar/spam.foo")
39+
# NTFS alternate data streams
40+
self.assertEqual(fn('C:\\foo:bar'), '///C:/foo%3Abar')
41+
self.assertEqual(fn('foo:bar'), 'foo%3Abar')
42+
# No drive letter
43+
self.assertEqual(fn("\\folder\\test\\"), '///folder/test/')
44+
self.assertEqual(fn("\\\\folder\\test\\"), '//folder/test/')
45+
self.assertEqual(fn("\\\\\\folder\\test\\"), '///folder/test/')
46+
self.assertEqual(fn('\\\\some\\share\\'), '//some/share/')
47+
self.assertEqual(fn('\\\\some\\share\\a\\b.c'), '//some/share/a/b.c')
48+
self.assertEqual(fn('\\\\some\\share\\a\\b%#c\xe9'), '//some/share/a/b%25%23c%C3%A9')
49+
# Alternate path separator
50+
self.assertEqual(fn('C:/a/b.c'), '///C:/a/b.c')
51+
self.assertEqual(fn('//some/share/a/b.c'), '//some/share/a/b.c')
52+
self.assertEqual(fn('//?/C:/dir'), '///C:/dir')
53+
self.assertEqual(fn('//?/unc/server/share/dir'), '//server/share/dir')
54+
# Round-tripping
55+
urls = ['///C:',
56+
'///folder/test/',
57+
'///C:/foo/bar/spam.foo']
58+
for url in urls:
59+
self.assertEqual(fn(nturl2path.url2pathname(url)), url)
60+
61+
def test_url2pathname(self):
62+
fn = nturl2path.url2pathname
63+
self.assertEqual(fn('/'), '\\')
64+
self.assertEqual(fn('/C:/'), 'C:\\')
65+
self.assertEqual(fn("///C|"), 'C:')
66+
self.assertEqual(fn("///C:"), 'C:')
67+
self.assertEqual(fn('///C:/'), 'C:\\')
68+
self.assertEqual(fn('/C|//'), 'C:\\\\')
69+
self.assertEqual(fn('///C|/path'), 'C:\\path')
70+
# No DOS drive
71+
self.assertEqual(fn("///C/test/"), '\\C\\test\\')
72+
self.assertEqual(fn("////C/test/"), '\\\\C\\test\\')
73+
# DOS drive paths
74+
self.assertEqual(fn('c:/path/to/file'), 'c:\\path\\to\\file')
75+
self.assertEqual(fn('C:/path/to/file'), 'C:\\path\\to\\file')
76+
self.assertEqual(fn('C:/path/to/file/'), 'C:\\path\\to\\file\\')
77+
self.assertEqual(fn('C:/path/to//file'), 'C:\\path\\to\\\\file')
78+
self.assertEqual(fn('C|/path/to/file'), 'C:\\path\\to\\file')
79+
self.assertEqual(fn('/C|/path/to/file'), 'C:\\path\\to\\file')
80+
self.assertEqual(fn('///C|/path/to/file'), 'C:\\path\\to\\file')
81+
self.assertEqual(fn("///C|/foo/bar/spam.foo"), 'C:\\foo\\bar\\spam.foo')
82+
# Colons in URI
83+
self.assertEqual(fn('///\u00e8|/'), '\u00e8:\\')
84+
self.assertEqual(fn('//host/share/spam.txt:eggs'), '\\\\host\\share\\spam.txt:eggs')
85+
self.assertEqual(fn('///c:/spam.txt:eggs'), 'c:\\spam.txt:eggs')
86+
# UNC paths
87+
self.assertEqual(fn('//server/path/to/file'), '\\\\server\\path\\to\\file')
88+
self.assertEqual(fn('////server/path/to/file'), '\\\\server\\path\\to\\file')
89+
self.assertEqual(fn('/////server/path/to/file'), '\\\\server\\path\\to\\file')
90+
# Localhost paths
91+
self.assertEqual(fn('//localhost/C:/path/to/file'), 'C:\\path\\to\\file')
92+
self.assertEqual(fn('//localhost/C|/path/to/file'), 'C:\\path\\to\\file')
93+
self.assertEqual(fn('//localhost/path/to/file'), '\\path\\to\\file')
94+
self.assertEqual(fn('//localhost//server/path/to/file'), '\\\\server\\path\\to\\file')
95+
# Percent-encoded forward slashes are preserved for backwards compatibility
96+
self.assertEqual(fn('C:/foo%2fbar'), 'C:\\foo/bar')
97+
self.assertEqual(fn('//server/share/foo%2fbar'), '\\\\server\\share\\foo/bar')
98+
# Round-tripping
99+
paths = ['C:',
100+
r'\C\test\\',
101+
r'C:\foo\bar\spam.foo']
102+
for path in paths:
103+
self.assertEqual(fn(nturl2path.pathname2url(path)), path)
104+
105+
106+
if __name__ == '__main__':
107+
unittest.main()

Lib/test/test_urllib2.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,6 @@ def test___all__(self):
4444
context = {}
4545
exec('from urllib.%s import *' % module, context)
4646
del context['__builtins__']
47-
if module == 'request' and os.name == 'nt':
48-
u, p = context.pop('url2pathname'), context.pop('pathname2url')
49-
self.assertEqual(u.__module__, 'nturl2path')
50-
self.assertEqual(p.__module__, 'nturl2path')
5147
for k, v in context.items():
5248
self.assertEqual(v.__module__, 'urllib.%s' % module,
5349
"%r is exposed in 'urllib.%s' but defined in %r" %

Lib/urllib/request.py

Lines changed: 56 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1646,36 +1646,62 @@ def data_open(self, req):
16461646

16471647
# Code move from the old urllib module
16481648

1649-
# Helper for non-unix systems
1650-
if os.name == 'nt':
1651-
from nturl2path import url2pathname, pathname2url
1652-
else:
1653-
def url2pathname(pathname):
1654-
"""OS-specific conversion from a relative URL of the 'file' scheme
1655-
to a file system path; not recommended for general use."""
1656-
if pathname[:3] == '///':
1657-
# URL has an empty authority section, so the path begins on the
1658-
# third character.
1659-
pathname = pathname[2:]
1660-
elif pathname[:12] == '//localhost/':
1661-
# Skip past 'localhost' authority.
1662-
pathname = pathname[11:]
1663-
encoding = sys.getfilesystemencoding()
1664-
errors = sys.getfilesystemencodeerrors()
1665-
return unquote(pathname, encoding=encoding, errors=errors)
1666-
1667-
def pathname2url(pathname):
1668-
"""OS-specific conversion from a file system path to a relative URL
1669-
of the 'file' scheme; not recommended for general use."""
1670-
if pathname[:1] == '/':
1671-
# Add explicitly empty authority to absolute path. If the path
1672-
# starts with exactly one slash then this change is mostly
1673-
# cosmetic, but if it begins with two or more slashes then this
1674-
# avoids interpreting the path as a URL authority.
1675-
pathname = '//' + pathname
1676-
encoding = sys.getfilesystemencoding()
1677-
errors = sys.getfilesystemencodeerrors()
1678-
return quote(pathname, encoding=encoding, errors=errors)
1649+
def url2pathname(url):
1650+
"""OS-specific conversion from a relative URL of the 'file' scheme
1651+
to a file system path; not recommended for general use."""
1652+
if url[:3] == '///':
1653+
# Empty authority section, so the path begins on the third character.
1654+
url = url[2:]
1655+
elif url[:12] == '//localhost/':
1656+
# Skip past 'localhost' authority.
1657+
url = url[11:]
1658+
1659+
if os.name == 'nt':
1660+
if url[:3] == '///':
1661+
# Skip past extra slash before UNC drive in URL path.
1662+
url = url[1:]
1663+
else:
1664+
if url[:1] == '/' and url[2:3] in (':', '|'):
1665+
# Skip past extra slash before DOS drive in URL path.
1666+
url = url[1:]
1667+
if url[1:2] == '|':
1668+
# Older URLs use a pipe after a drive letter
1669+
url = url[:1] + ':' + url[2:]
1670+
url = url.replace('/', '\\')
1671+
encoding = sys.getfilesystemencoding()
1672+
errors = sys.getfilesystemencodeerrors()
1673+
return unquote(url, encoding=encoding, errors=errors)
1674+
1675+
1676+
def pathname2url(pathname):
1677+
"""OS-specific conversion from a file system path to a relative URL
1678+
of the 'file' scheme; not recommended for general use."""
1679+
if os.name == 'nt':
1680+
pathname = pathname.replace('\\', '/')
1681+
encoding = sys.getfilesystemencoding()
1682+
errors = sys.getfilesystemencodeerrors()
1683+
drive, root, tail = os.path.splitroot(pathname)
1684+
if drive:
1685+
# First, clean up some special forms. We are going to sacrifice the
1686+
# additional information anyway
1687+
if drive[:4] == '//?/':
1688+
drive = drive[4:]
1689+
if drive[:4].upper() == 'UNC/':
1690+
drive = '//' + drive[4:]
1691+
if drive[1:] == ':':
1692+
# DOS drive specified. Add three slashes to the start, producing
1693+
# an authority section with a zero-length authority, and a path
1694+
# section starting with a single slash.
1695+
drive = '///' + drive
1696+
drive = quote(drive, encoding=encoding, errors=errors, safe='/:')
1697+
elif root:
1698+
# Add explicitly empty authority to absolute path. If the path
1699+
# starts with exactly one slash then this change is mostly
1700+
# cosmetic, but if it begins with two or more slashes then this
1701+
# avoids interpreting the path as a URL authority.
1702+
root = '//' + root
1703+
tail = quote(tail, encoding=encoding, errors=errors)
1704+
return drive + root + tail
16791705

16801706

16811707
# Utility functions
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Deprecate the :mod:`!nturl2path` module. Call
2+
:func:`urllib.request.url2pathname` and :func:`~urllib.request.pathname2url`
3+
instead.

0 commit comments

Comments
 (0)