Skip to content

Commit

Permalink
pythongh-90329: Add _winapi.GetLongPathName and GetShortPathName and …
Browse files Browse the repository at this point in the history
…use in venv to reduce warnings (pythonGH-117817)
  • Loading branch information
zooba committed Apr 15, 2024
1 parent 6a4c06b commit efe06ac
Show file tree
Hide file tree
Showing 6 changed files with 494 additions and 3 deletions.
105 changes: 104 additions & 1 deletion Lib/test/test_venv.py
Expand Up @@ -22,7 +22,8 @@
requires_subprocess, is_emscripten, is_wasi,
requires_venv_with_pip, TEST_HOME_DIR,
requires_resource, copy_python_src_ignore)
from test.support.os_helper import (can_symlink, EnvironmentVarGuard, rmtree)
from test.support.os_helper import (can_symlink, EnvironmentVarGuard, rmtree,
TESTFN)
import unittest
import venv
from unittest.mock import patch, Mock
Expand Down Expand Up @@ -639,6 +640,108 @@ def test_activate_shell_script_has_no_dos_newlines(self):
error_message = f"CR LF found in line {i}"
self.assertFalse(line.endswith(b'\r\n'), error_message)

<<<<<<< HEAD
=======
@requireVenvCreate
def test_scm_ignore_files_git(self):
"""
Test that a .gitignore file is created when "git" is specified.
The file should contain a `*\n` line.
"""
self.run_with_capture(venv.create, self.env_dir,
scm_ignore_files={'git'})
file_lines = self.get_text_file_contents('.gitignore').splitlines()
self.assertIn('*', file_lines)

@requireVenvCreate
def test_create_scm_ignore_files_multiple(self):
"""
Test that ``scm_ignore_files`` can work with multiple SCMs.
"""
bzrignore_name = ".bzrignore"
contents = "# For Bazaar.\n*\n"

class BzrEnvBuilder(venv.EnvBuilder):
def create_bzr_ignore_file(self, context):
gitignore_path = os.path.join(context.env_dir, bzrignore_name)
with open(gitignore_path, 'w', encoding='utf-8') as file:
file.write(contents)

builder = BzrEnvBuilder(scm_ignore_files={'git', 'bzr'})
self.run_with_capture(builder.create, self.env_dir)

gitignore_lines = self.get_text_file_contents('.gitignore').splitlines()
self.assertIn('*', gitignore_lines)

bzrignore = self.get_text_file_contents(bzrignore_name)
self.assertEqual(bzrignore, contents)

@requireVenvCreate
def test_create_scm_ignore_files_empty(self):
"""
Test that no default ignore files are created when ``scm_ignore_files``
is empty.
"""
# scm_ignore_files is set to frozenset() by default.
self.run_with_capture(venv.create, self.env_dir)
with self.assertRaises(FileNotFoundError):
self.get_text_file_contents('.gitignore')

self.assertIn("--without-scm-ignore-files",
self.get_text_file_contents('pyvenv.cfg'))

@requireVenvCreate
def test_cli_with_scm_ignore_files(self):
"""
Test that default SCM ignore files are created by default via the CLI.
"""
self.run_with_capture(venv.main, ['--without-pip', self.env_dir])

gitignore_lines = self.get_text_file_contents('.gitignore').splitlines()
self.assertIn('*', gitignore_lines)

@requireVenvCreate
def test_cli_without_scm_ignore_files(self):
"""
Test that ``--without-scm-ignore-files`` doesn't create SCM ignore files.
"""
args = ['--without-pip', '--without-scm-ignore-files', self.env_dir]
self.run_with_capture(venv.main, args)

with self.assertRaises(FileNotFoundError):
self.get_text_file_contents('.gitignore')

def test_venv_same_path(self):
same_path = venv.EnvBuilder._same_path
if sys.platform == 'win32':
# Case-insensitive, and handles short/long names
tests = [
(True, TESTFN, TESTFN),
(True, TESTFN.lower(), TESTFN.upper()),
]
import _winapi
# ProgramFiles is the most reliable path that will have short/long
progfiles = os.getenv('ProgramFiles')
if progfiles:
tests = [
*tests,
(True, progfiles, progfiles),
(True, _winapi.GetShortPathName(progfiles), _winapi.GetLongPathName(progfiles)),
]
else:
# Just a simple case-sensitive comparison
tests = [
(True, TESTFN, TESTFN),
(False, TESTFN.lower(), TESTFN.upper()),
]
for r, path1, path2 in tests:
with self.subTest(f"{path1}-{path2}"):
if r:
self.assertTrue(same_path(path1, path2))
else:
self.assertFalse(same_path(path1, path2))

>>>>>>> 185999bb3a (gh-90329: Add _winapi.GetLongPathName and GetShortPathName and use in venv to reduce warnings (GH-117817))
@requireVenvCreate
class EnsurePipTest(BaseTest):
"""Test venv module installation of pip."""
Expand Down
129 changes: 129 additions & 0 deletions Lib/test/test_winapi.py
@@ -0,0 +1,129 @@
# Test the Windows-only _winapi module

import os
import pathlib
import random
import re
import threading
import time
import unittest
from test.support import import_helper

_winapi = import_helper.import_module('_winapi', required_on=['win'])

MAXIMUM_WAIT_OBJECTS = 64
MAXIMUM_BATCHED_WAIT_OBJECTS = (MAXIMUM_WAIT_OBJECTS - 1) ** 2

class WinAPIBatchedWaitForMultipleObjectsTests(unittest.TestCase):
def _events_waitall_test(self, n):
evts = [_winapi.CreateEventW(0, False, False, None) for _ in range(n)]

with self.assertRaises(TimeoutError):
_winapi.BatchedWaitForMultipleObjects(evts, True, 100)

# Ensure no errors raised when all are triggered
for e in evts:
_winapi.SetEvent(e)
try:
_winapi.BatchedWaitForMultipleObjects(evts, True, 100)
except TimeoutError:
self.fail("expected wait to complete immediately")

# Choose 8 events to set, distributed throughout the list, to make sure
# we don't always have them in the first chunk
chosen = [i * (len(evts) // 8) for i in range(8)]

# Replace events with invalid handles to make sure we fail
for i in chosen:
old_evt = evts[i]
evts[i] = -1
with self.assertRaises(OSError):
_winapi.BatchedWaitForMultipleObjects(evts, True, 100)
evts[i] = old_evt


def _events_waitany_test(self, n):
evts = [_winapi.CreateEventW(0, False, False, None) for _ in range(n)]

with self.assertRaises(TimeoutError):
_winapi.BatchedWaitForMultipleObjects(evts, False, 100)

# Choose 8 events to set, distributed throughout the list, to make sure
# we don't always have them in the first chunk
chosen = [i * (len(evts) // 8) for i in range(8)]

# Trigger one by one. They are auto-reset events, so will only trigger once
for i in chosen:
with self.subTest(f"trigger event {i} of {len(evts)}"):
_winapi.SetEvent(evts[i])
triggered = _winapi.BatchedWaitForMultipleObjects(evts, False, 10000)
self.assertSetEqual(set(triggered), {i})

# Trigger all at once. This may require multiple calls
for i in chosen:
_winapi.SetEvent(evts[i])
triggered = set()
while len(triggered) < len(chosen):
triggered.update(_winapi.BatchedWaitForMultipleObjects(evts, False, 10000))
self.assertSetEqual(triggered, set(chosen))

# Replace events with invalid handles to make sure we fail
for i in chosen:
with self.subTest(f"corrupt event {i} of {len(evts)}"):
old_evt = evts[i]
evts[i] = -1
with self.assertRaises(OSError):
_winapi.BatchedWaitForMultipleObjects(evts, False, 100)
evts[i] = old_evt


def test_few_events_waitall(self):
self._events_waitall_test(16)

def test_many_events_waitall(self):
self._events_waitall_test(256)

def test_max_events_waitall(self):
self._events_waitall_test(MAXIMUM_BATCHED_WAIT_OBJECTS)


def test_few_events_waitany(self):
self._events_waitany_test(16)

def test_many_events_waitany(self):
self._events_waitany_test(256)

def test_max_events_waitany(self):
self._events_waitany_test(MAXIMUM_BATCHED_WAIT_OBJECTS)


class WinAPITests(unittest.TestCase):
def test_getlongpathname(self):
testfn = pathlib.Path(os.getenv("ProgramFiles")).parents[-1] / "PROGRA~1"
if not os.path.isdir(testfn):
raise unittest.SkipTest("require x:\\PROGRA~1 to test")

# pathlib.Path will be rejected - only str is accepted
with self.assertRaises(TypeError):
_winapi.GetLongPathName(testfn)

actual = _winapi.GetLongPathName(os.fsdecode(testfn))

# Can't assume that PROGRA~1 expands to any particular variation, so
# ensure it matches any one of them.
candidates = set(testfn.parent.glob("Progra*"))
self.assertIn(pathlib.Path(actual), candidates)

def test_getshortpathname(self):
testfn = pathlib.Path(os.getenv("ProgramFiles"))
if not os.path.isdir(testfn):
raise unittest.SkipTest("require '%ProgramFiles%' to test")

# pathlib.Path will be rejected - only str is accepted
with self.assertRaises(TypeError):
_winapi.GetShortPathName(testfn)

actual = _winapi.GetShortPathName(os.fsdecode(testfn))

# Should contain "PROGRA~" but we can't predict the number
self.assertIsNotNone(re.match(r".\:\\PROGRA~\d", actual.upper()), actual)
29 changes: 28 additions & 1 deletion Lib/venv/__init__.py
Expand Up @@ -102,6 +102,33 @@ def _venv_path(self, env_dir, name):
}
return sysconfig.get_path(name, scheme='venv', vars=vars)

@classmethod
def _same_path(cls, path1, path2):
"""Check whether two paths appear the same.
Whether they refer to the same file is irrelevant; we're testing for
whether a human reader would look at the path string and easily tell
that they're the same file.
"""
if sys.platform == 'win32':
if os.path.normcase(path1) == os.path.normcase(path2):
return True
# gh-90329: Don't display a warning for short/long names
import _winapi
try:
path1 = _winapi.GetLongPathName(os.fsdecode(path1))
except OSError:
pass
try:
path2 = _winapi.GetLongPathName(os.fsdecode(path2))
except OSError:
pass
if os.path.normcase(path1) == os.path.normcase(path2):
return True
return False
else:
return path1 == path2

def ensure_directories(self, env_dir):
"""
Create the directories for the environment.
Expand Down Expand Up @@ -162,7 +189,7 @@ def create_if_needed(d):
# bpo-45337: Fix up env_exec_cmd to account for file system redirections.
# Some redirects only apply to CreateFile and not CreateProcess
real_env_exe = os.path.realpath(context.env_exe)
if os.path.normcase(real_env_exe) != os.path.normcase(context.env_exe):
if not self._same_path(real_env_exe, context.env_exe):
logger.warning('Actual environment location may have moved due to '
'redirects, links or junctions.\n'
' Requested location: "%s"\n'
Expand Down
@@ -0,0 +1,5 @@
Suppress the warning displayed on virtual environment creation when the
requested and created paths differ only by a short (8.3 style) name.
Warnings will continue to be shown if a junction or symlink in the path
caused the venv to be created in a different location than originally
requested.

0 comments on commit efe06ac

Please sign in to comment.