Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Upgrade nt_path from v3.14.2
  • Loading branch information
youknowone committed Jan 24, 2026
commit 2ac1b04914acb05fc6f03cff3fc29c558a88e3e2
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
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
25 changes: 18 additions & 7 deletions Lib/test/test_ntpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@
import sys
import unittest
import warnings
from ntpath import ALLOW_MISSING
from test import support
from test.support import cpython_only, os_helper
from test.support import TestFailed, is_emscripten
from test.support import os_helper
from ntpath import ALLOW_MISSING
from test.support.os_helper import FakePath
from test import test_genericpath
from tempfile import TemporaryFile
Expand Down Expand Up @@ -59,7 +58,7 @@ def tester(fn, wantResult):
fn = fn.replace("\\", "\\\\")
gotResult = eval(fn)
if wantResult != gotResult and _norm(wantResult) != _norm(gotResult):
raise TestFailed("%s should return: %s but returned: %s" \
raise support.TestFailed("%s should return: %s but returned: %s" \
%(str(fn), str(wantResult), str(gotResult)))

# then with bytes
Expand All @@ -75,7 +74,7 @@ def tester(fn, wantResult):
warnings.simplefilter("ignore", DeprecationWarning)
gotResult = eval(fn)
if _norm(wantResult) != _norm(gotResult):
raise TestFailed("%s should return: %s but returned: %s" \
raise support.TestFailed("%s should return: %s but returned: %s" \
%(str(fn), str(wantResult), repr(gotResult)))


Expand Down Expand Up @@ -1022,6 +1021,19 @@ def check(value, expected):
check('%spam%bar', '%sbar' % nonascii)
check('%{}%bar'.format(nonascii), 'ham%sbar' % nonascii)

@support.requires_resource('cpu')
def test_expandvars_large(self):
expandvars = ntpath.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%A'*n), 'BA'*n)
self.assertEqual(expandvars("''"*n + '%%'), "''"*n + '%')
self.assertEqual(expandvars("%%"*n), "%"*n)
self.assertEqual(expandvars("$$"*n), "$"*n)

def test_expanduser(self):
tester('ntpath.expanduser("test")', 'test')

Expand Down Expand Up @@ -1229,7 +1241,6 @@ def check_error(paths, expected):
self.assertRaises(TypeError, ntpath.commonpath, ['C:\\Foo', b'Foo\\Baz'])
self.assertRaises(TypeError, ntpath.commonpath, ['Foo', b'C:\\Foo\\Baz'])

@unittest.skipIf(is_emscripten, "Emscripten cannot fstat unnamed files.")
def test_sameopenfile(self):
with TemporaryFile() as tf1, TemporaryFile() as tf2:
# Make sure the same file is really the same
Expand Down Expand Up @@ -1440,7 +1451,7 @@ def test_con_device(self):
self.assertTrue(os.path.exists(r"\\.\CON"))

@unittest.skipIf(sys.platform != 'win32', "Fast paths are only for win32")
@cpython_only
@support.cpython_only
def test_fast_paths_in_use(self):
# There are fast paths of these functions implemented in posixmodule.c.
# Confirm that they are being used, and not the Python fallbacks in
Expand Down