Skip to content
Merged
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
126 changes: 41 additions & 85 deletions Lib/ntpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,17 +400,23 @@ def expanduser(path):
# XXX With COMMAND.COM you can use any characters in a variable name,
# XXX except '^|<>='.

_varpattern = r"'[^']*'?|%(%|[^%]*%?)|\$(\$|[-\w]+|\{[^}]*\}?)"
_varsub = None
_varsubb = None

def expandvars(path):
"""Expand shell variables of the forms $var, ${var} and %var%.
Unknown variables are left unchanged."""
path = os.fspath(path)
global _varsub, _varsubb
if isinstance(path, bytes):
if b'$' not in path and b'%' not in path:
return path
import string
varchars = bytes(string.ascii_letters + string.digits + '_-', 'ascii')
quote = b'\''
if not _varsubb:
import re
_varsubb = re.compile(_varpattern.encode(), re.ASCII).sub
sub = _varsubb
percent = b'%'
brace = b'{'
rbrace = b'}'
Expand All @@ -419,94 +425,44 @@ def expandvars(path):
else:
if '$' not in path and '%' not in path:
return path
import string
varchars = string.ascii_letters + string.digits + '_-'
quote = '\''
if not _varsub:
import re
_varsub = re.compile(_varpattern, re.ASCII).sub
sub = _varsub
percent = '%'
brace = '{'
rbrace = '}'
dollar = '$'
environ = os.environ
res = path[:0]
index = 0
pathlen = len(path)
while index < pathlen:
c = path[index:index+1]
if c == quote: # no expansion within single quotes
path = path[index + 1:]
pathlen = len(path)
try:
index = path.index(c)
res += c + path[:index + 1]
except ValueError:
res += c + path
index = pathlen - 1
elif c == percent: # variable or '%'
if path[index + 1:index + 2] == percent:
res += c
index += 1
else:
path = path[index+1:]
pathlen = len(path)
try:
index = path.index(percent)
except ValueError:
res += percent + path
index = pathlen - 1
else:
var = path[:index]
try:
if environ is None:
value = os.fsencode(os.environ[os.fsdecode(var)])
else:
value = environ[var]
except KeyError:
value = percent + var + percent
res += value
elif c == dollar: # variable or '$$'
if path[index + 1:index + 2] == dollar:
res += c
index += 1
elif path[index + 1:index + 2] == brace:
path = path[index+2:]
pathlen = len(path)
try:
index = path.index(rbrace)
except ValueError:
res += dollar + brace + path
index = pathlen - 1
else:
var = path[:index]
try:
if environ is None:
value = os.fsencode(os.environ[os.fsdecode(var)])
else:
value = environ[var]
except KeyError:
value = dollar + brace + var + rbrace
res += value
else:
var = path[:0]
index += 1
c = path[index:index + 1]
while c and c in varchars:
var += c
index += 1
c = path[index:index + 1]
try:
if environ is None:
value = os.fsencode(os.environ[os.fsdecode(var)])
else:
value = environ[var]
except KeyError:
value = dollar + var
res += value
if c:
index -= 1

def repl(m):
lastindex = m.lastindex
if lastindex is None:
return m[0]
name = m[lastindex]
if lastindex == 1:
if name == percent:
return name
if not name.endswith(percent):
return m[0]
name = name[:-1]
else:
res += c
index += 1
return res
if name == dollar:
return name
if name.startswith(brace):
if not name.endswith(rbrace):
return m[0]
name = name[1:-1]

try:
if environ is None:
return os.fsencode(os.environ[os.fsdecode(name)])
else:
return environ[name]
except KeyError:
return m[0]

return sub(repl, path)


# Normalize a path, e.g. A//B, A/./B and A/foo/../B all become A\B.
Expand Down
59 changes: 32 additions & 27 deletions Lib/nturl2path.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@
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__,
message=f"{warnings._DEPRECATED_MSG}; use 'urllib.request' instead",
remove=(3, 19))

def url2pathname(url):
"""OS-specific conversion from a relative URL of the 'file' scheme
Expand All @@ -14,7 +22,7 @@ def url2pathname(url):
# ///C:/foo/bar/spam.foo
# become
# C:\foo\bar\spam.foo
import string, urllib.parse
import urllib.parse
if url[:3] == '///':
# URL has an empty authority section, so the path begins on the third
# character.
Expand All @@ -25,19 +33,14 @@ def url2pathname(url):
if url[:3] == '///':
# Skip past extra slash before UNC drive in URL path.
url = url[1:]
# Windows itself uses ":" even in URLs.
url = url.replace(':', '|')
if not '|' in url:
# No drive specifier, just convert slashes
# make sure not to convert quoted slashes :-)
return urllib.parse.unquote(url.replace('/', '\\'))
comp = url.split('|')
if len(comp) != 2 or comp[0][-1] not in string.ascii_letters:
error = 'Bad URL: ' + url
raise OSError(error)
drive = comp[0][-1].upper()
tail = urllib.parse.unquote(comp[1].replace('/', '\\'))
return drive + ':' + tail
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:]
return urllib.parse.unquote(url.replace('/', '\\'))

def pathname2url(p):
"""OS-specific conversion from a file system path to a relative URL
Expand All @@ -46,6 +49,7 @@ def pathname2url(p):
# C:\foo\bar\spam.foo
# becomes
# ///C:/foo/bar/spam.foo
import ntpath
import urllib.parse
# First, clean up some special forms. We are going to sacrifice
# the additional information anyway
Expand All @@ -54,16 +58,17 @@ def pathname2url(p):
p = p[4:]
if p[:4].upper() == 'UNC/':
p = '//' + p[4:]
elif p[1:2] != ':':
raise OSError('Bad path: ' + p)
if not ':' in p:
# No DOS drive specified, just quote the pathname
return urllib.parse.quote(p)
comp = p.split(':', maxsplit=2)
if len(comp) != 2 or len(comp[0]) > 1:
error = 'Bad path: ' + p
raise OSError(error)
drive, root, tail = ntpath.splitroot(p)
if drive:
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.
drive = f'///{drive}'
drive = urllib.parse.quote(drive, safe='/:')
elif root:
# Add explicitly empty authority to path beginning with one slash.
root = f'//{root}'

drive = urllib.parse.quote(comp[0].upper())
tail = urllib.parse.quote(comp[1])
return '///' + drive + ':' + tail
tail = urllib.parse.quote(tail)
return drive + root + tail
30 changes: 20 additions & 10 deletions Lib/test/test_genericpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
import sys
import unittest
import warnings
from test.support import (
is_apple, is_emscripten, os_helper, warnings_helper
)
from test import support
from test.support import os_helper
from test.support import warnings_helper
from test.support.script_helper import assert_python_ok
from test.support.os_helper import FakePath

Expand Down Expand Up @@ -92,8 +92,8 @@ def test_commonprefix(self):
for s1 in testlist:
for s2 in testlist:
p = commonprefix([s1, s2])
self.assertTrue(s1.startswith(p))
self.assertTrue(s2.startswith(p))
self.assertStartsWith(s1, p)
self.assertStartsWith(s2, p)
if s1 != s2:
n = len(p)
self.assertNotEqual(s1[n:n+1], s2[n:n+1])
Expand Down Expand Up @@ -161,7 +161,6 @@ def test_exists(self):
self.assertIs(self.pathmodule.lexists(path=filename), True)

@unittest.skipUnless(hasattr(os, "pipe"), "requires os.pipe()")
@unittest.skipIf(is_emscripten, "Emscripten pipe fds have no stat")
def test_exists_fd(self):
r, w = os.pipe()
try:
Expand All @@ -171,8 +170,7 @@ def test_exists_fd(self):
os.close(w)
self.assertFalse(self.pathmodule.exists(r))

# TODO: RUSTPYTHON
@unittest.expectedFailure
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_exists_bool(self):
for fd in False, True:
with self.assertWarnsRegex(RuntimeWarning,
Expand Down Expand Up @@ -352,7 +350,6 @@ def test_invalid_paths(self):
with self.assertRaisesRegex(ValueError, 'embedded null'):
func(b'/tmp\x00abcds')


# Following TestCase is not supposed to be run from test_genericpath.
# It is inherited by other test modules (ntpath, posixpath).

Expand Down Expand Up @@ -449,6 +446,19 @@ def check(value, expected):
os.fsencode('$bar%s bar' % nonascii))
check(b'$spam}bar', os.fsencode('%s}bar' % nonascii))

@support.requires_resource('cpu')
def test_expandvars_large(self):
expandvars = self.pathmodule.expandvars
with os_helper.EnvironmentVarGuard() as env:
env.clear()
env["A"] = "B"
n = 100_000
self.assertEqual(expandvars('$A'*n), 'B'*n)
self.assertEqual(expandvars('${A}'*n), 'B'*n)
self.assertEqual(expandvars('$A!'*n), 'B!'*n)
self.assertEqual(expandvars('${A}A'*n), 'BA'*n)
self.assertEqual(expandvars('${'*10*n), '${'*10*n)

def test_abspath(self):
self.assertIn("foo", self.pathmodule.abspath("foo"))
with warnings.catch_warnings():
Expand Down Expand Up @@ -506,7 +516,7 @@ def test_nonascii_abspath(self):
# directory (when the bytes name is used).
and sys.platform not in {
"win32", "emscripten", "wasi"
} and not is_apple
} and not support.is_apple
):
name = os_helper.TESTFN_UNDECODABLE
elif os_helper.TESTFN_NONASCII:
Expand Down
Loading
Loading