mirror of
https://github.com/microsoft/debugpy.git
synced 2025-12-23 08:48:12 +00:00
Support dealing with paths with inconsistent casing on Mac OS. Fixes #1031
This commit is contained in:
parent
ac6465760a
commit
ae189da2a8
3 changed files with 164 additions and 63 deletions
|
|
@ -43,7 +43,7 @@ r'''
|
|||
|
||||
from _pydev_bundle import pydev_log
|
||||
from _pydevd_bundle.pydevd_constants import DebugInfoHolder, IS_WINDOWS, IS_JYTHON, \
|
||||
DISABLE_FILE_VALIDATION, is_true_in_env
|
||||
DISABLE_FILE_VALIDATION, is_true_in_env, IS_MAC
|
||||
from _pydev_bundle._pydev_filesystem_encoding import getfilesystemencoding
|
||||
from _pydevd_bundle.pydevd_comm_constants import file_system_encoding, filesystem_encoding_is_utf8
|
||||
from _pydev_bundle.pydev_log import error_once
|
||||
|
|
@ -124,6 +124,67 @@ convert_to_long_pathname = lambda filename:filename
|
|||
convert_to_short_pathname = lambda filename:filename
|
||||
get_path_with_real_case = lambda filename:filename
|
||||
|
||||
# Note that we have a cache for previous list dirs... the only case where this may be an
|
||||
# issue is if the user actually changes the case of an existing file on while
|
||||
# the debugger is executing (as this seems very unlikely and the cache can save a
|
||||
# reasonable time -- especially on mapped drives -- it seems nice to have it).
|
||||
_listdir_cache = {}
|
||||
|
||||
# May be changed during tests.
|
||||
os_listdir = os.listdir
|
||||
|
||||
|
||||
def _resolve_listing(resolved, iter_parts_lowercase, cache=_listdir_cache):
|
||||
while True: # Note: while True to make iterative and not recursive
|
||||
try:
|
||||
resolve_lowercase = next(iter_parts_lowercase) # must be lowercase already
|
||||
except StopIteration:
|
||||
return resolved
|
||||
|
||||
resolved_lower = resolved.lower()
|
||||
|
||||
resolved_joined = cache.get((resolved_lower, resolve_lowercase))
|
||||
if resolved_joined is None:
|
||||
dir_contents = cache.get(resolved_lower)
|
||||
if dir_contents is None:
|
||||
dir_contents = cache[resolved_lower] = os_listdir(resolved)
|
||||
|
||||
for filename in dir_contents:
|
||||
if filename.lower() == resolve_lowercase:
|
||||
resolved_joined = os.path.join(resolved, filename)
|
||||
cache[(resolved_lower, resolve_lowercase)] = resolved_joined
|
||||
break
|
||||
else:
|
||||
raise FileNotFoundError('Unable to find: %s in %s. Dir Contents: %s' % (
|
||||
resolve_lowercase, resolved, dir_contents))
|
||||
|
||||
resolved = resolved_joined
|
||||
|
||||
|
||||
def _resolve_listing_parts(resolved, parts_in_lowercase, filename):
|
||||
try:
|
||||
if parts_in_lowercase == ['']:
|
||||
return resolved
|
||||
return _resolve_listing(resolved, iter(parts_in_lowercase))
|
||||
except FileNotFoundError:
|
||||
_listdir_cache.clear()
|
||||
# Retry once after clearing the cache we have.
|
||||
try:
|
||||
return _resolve_listing(resolved, iter(parts_in_lowercase))
|
||||
except FileNotFoundError:
|
||||
if os_path_exists(filename):
|
||||
# This is really strange, ask the user to report as error.
|
||||
pydev_log.critical(
|
||||
'pydev debugger: critical: unable to get real case for file. Details:\n'
|
||||
'filename: %s\ndrive: %s\nparts: %s\n'
|
||||
'(please create a ticket in the tracker to address this).',
|
||||
filename, resolved, parts_in_lowercase
|
||||
)
|
||||
pydev_log.exception()
|
||||
# Don't fail, just return the original file passed.
|
||||
return filename
|
||||
|
||||
|
||||
if sys.platform == 'win32':
|
||||
try:
|
||||
import ctypes
|
||||
|
|
@ -155,38 +216,6 @@ if sys.platform == 'win32':
|
|||
|
||||
return filename
|
||||
|
||||
# Note that we have a cache for previous list dirs... the only case where this may be an
|
||||
# issue is if the user actually changes the case of an existing file on windows while
|
||||
# the debugger is executing (as this seems very unlikely and the cache can save a
|
||||
# reasonable time -- especially on mapped drives -- it seems nice to have it).
|
||||
_listdir_cache = {}
|
||||
|
||||
def _resolve_listing(resolved, iter_parts, cache=_listdir_cache):
|
||||
while True: # Note: while True to make iterative and not recursive
|
||||
try:
|
||||
resolve_lowercase = next(iter_parts) # must be lowercase already
|
||||
except StopIteration:
|
||||
return resolved
|
||||
|
||||
resolved_lower = resolved.lower()
|
||||
|
||||
resolved_joined = cache.get((resolved_lower, resolve_lowercase))
|
||||
if resolved_joined is None:
|
||||
dir_contents = cache.get(resolved_lower)
|
||||
if dir_contents is None:
|
||||
dir_contents = cache[resolved_lower] = os.listdir(resolved)
|
||||
|
||||
for filename in dir_contents:
|
||||
if filename.lower() == resolve_lowercase:
|
||||
resolved_joined = os.path.join(resolved, filename)
|
||||
cache[(resolved_lower, resolve_lowercase)] = resolved_joined
|
||||
break
|
||||
else:
|
||||
raise FileNotFoundError('Unable to find: %s in %s' % (
|
||||
resolve_lowercase, resolved))
|
||||
|
||||
resolved = resolved_joined
|
||||
|
||||
def _get_path_with_real_case(filename):
|
||||
# Note: this previously made:
|
||||
# convert_to_long_pathname(convert_to_short_pathname(filename))
|
||||
|
|
@ -206,28 +235,7 @@ if sys.platform == 'win32':
|
|||
parts = parts[1:]
|
||||
drive += os.path.sep
|
||||
parts = parts.lower().split(os.path.sep)
|
||||
|
||||
try:
|
||||
if parts == ['']:
|
||||
return drive
|
||||
return _resolve_listing(drive, iter(parts))
|
||||
except FileNotFoundError:
|
||||
_listdir_cache.clear()
|
||||
# Retry once after clearing the cache we have.
|
||||
try:
|
||||
return _resolve_listing(drive, iter(parts))
|
||||
except FileNotFoundError:
|
||||
if os_path_exists(filename):
|
||||
# This is really strange, ask the user to report as error.
|
||||
pydev_log.critical(
|
||||
'pydev debugger: critical: unable to get real case for file. Details:\n'
|
||||
'filename: %s\ndrive: %s\nparts: %s\n'
|
||||
'(please create a ticket in the tracker to address this).',
|
||||
filename, drive, parts
|
||||
)
|
||||
pydev_log.exception()
|
||||
# Don't fail, just return the original file passed.
|
||||
return filename
|
||||
return _resolve_listing_parts(drive, parts, filename)
|
||||
|
||||
# Check that it actually works
|
||||
_get_path_with_real_case(__file__)
|
||||
|
|
@ -243,11 +251,29 @@ if sys.platform == 'win32':
|
|||
elif IS_JYTHON and IS_WINDOWS:
|
||||
|
||||
def get_path_with_real_case(filename):
|
||||
if filename.startswith('<'):
|
||||
return filename
|
||||
|
||||
from java.io import File # noqa
|
||||
f = File(filename)
|
||||
ret = f.getCanonicalPath()
|
||||
return ret
|
||||
|
||||
elif IS_MAC:
|
||||
|
||||
def get_path_with_real_case(filename):
|
||||
if filename.startswith('<') or not os_path_exists(filename):
|
||||
return filename # Not much we can do.
|
||||
|
||||
parts = filename.lower().split('/')
|
||||
|
||||
found = ''
|
||||
while parts and parts[0] == '':
|
||||
found += '/'
|
||||
parts = parts[1:]
|
||||
|
||||
return _resolve_listing_parts(found, parts, filename)
|
||||
|
||||
if IS_JYTHON:
|
||||
|
||||
def _normcase_windows(filename):
|
||||
|
|
@ -286,6 +312,13 @@ elif _filename_normalization == 'none':
|
|||
elif IS_WINDOWS:
|
||||
_default_normcase = _normcase_windows
|
||||
|
||||
elif IS_MAC:
|
||||
|
||||
def _normcase_lower(filename):
|
||||
return filename.lower()
|
||||
|
||||
_default_normcase = _normcase_lower
|
||||
|
||||
else:
|
||||
_default_normcase = _normcase_linux
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# coding: utf-8
|
||||
import os.path
|
||||
from _pydevd_bundle.pydevd_constants import IS_WINDOWS
|
||||
from _pydevd_bundle.pydevd_constants import IS_WINDOWS, IS_MAC
|
||||
import io
|
||||
from _pydev_bundle.pydev_log import log_context
|
||||
import pytest
|
||||
|
|
@ -14,6 +14,62 @@ def _reset_ide_os():
|
|||
set_ide_os('WINDOWS' if sys.platform == 'win32' else 'UNIX')
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform != 'win32', reason='Windows-only test.')
|
||||
def test_get_path_with_real_case_windows_unc_path(monkeypatch):
|
||||
import pydevd_file_utils
|
||||
from pydevd_file_utils import get_path_with_real_case
|
||||
|
||||
def temp_listdir(d):
|
||||
# When we have a UNC drive in windows the "drive" is something as:
|
||||
# \\MACHINE_NAME\MOUNT_POINT\
|
||||
if d == '\\\\A\\B\\':
|
||||
return ['Cc']
|
||||
raise AssertionError('Unexpected: %s' % (d,))
|
||||
|
||||
monkeypatch.setattr(pydevd_file_utils, 'os_path_exists', lambda *args: True)
|
||||
monkeypatch.setattr(pydevd_file_utils, 'os_listdir', temp_listdir)
|
||||
assert get_path_with_real_case(r'\\a\b\cc') == r'\\A\B\Cc'
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform != 'win32', reason='Windows-only test.')
|
||||
def test_get_path_with_real_case_windows_slashes_drive(tmpdir):
|
||||
from pydevd_file_utils import get_path_with_real_case
|
||||
test_dir = str(tmpdir.mkdir("Test_Convert_Utilities")).lower()
|
||||
real_case = get_path_with_real_case(test_dir)
|
||||
assert real_case.endswith("Test_Convert_Utilities")
|
||||
|
||||
prefix = '\\\\?\\'
|
||||
path = prefix + test_dir
|
||||
real_case = get_path_with_real_case(path)
|
||||
assert real_case.endswith("Test_Convert_Utilities")
|
||||
assert path.startswith(prefix)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not IS_MAC, reason='Mac-only test.')
|
||||
def test_get_path_with_real_case_mac_os(tmpdir):
|
||||
from pydevd_file_utils import get_path_with_real_case
|
||||
test_dir = str(tmpdir.mkdir("Test_Convert_Utilities")).lower()
|
||||
real_case = get_path_with_real_case(test_dir)
|
||||
assert real_case.endswith("Test_Convert_Utilities")
|
||||
|
||||
|
||||
@pytest.mark.skipif(not IS_MAC, reason='Mac-only test.')
|
||||
def test_double_slash_mac(monkeypatch):
|
||||
import pydevd_file_utils
|
||||
from pydevd_file_utils import get_path_with_real_case
|
||||
|
||||
def temp_listdir(d):
|
||||
if d == '//':
|
||||
return ['A']
|
||||
if d == '//A':
|
||||
return ['Bb']
|
||||
raise AssertionError('Unexpected: %s' % (d,))
|
||||
|
||||
monkeypatch.setattr(pydevd_file_utils, 'os_path_exists', lambda *args: True)
|
||||
monkeypatch.setattr(pydevd_file_utils, 'os_listdir', temp_listdir)
|
||||
assert get_path_with_real_case(r'//a/bb') == r'//A/Bb'
|
||||
|
||||
|
||||
def test_convert_utilities(tmpdir):
|
||||
import pydevd_file_utils
|
||||
|
||||
|
|
@ -60,8 +116,12 @@ def test_convert_utilities(tmpdir):
|
|||
assert with_real_case.endswith('Test_Convert_Utilities')
|
||||
assert '~' not in with_real_case
|
||||
|
||||
elif IS_MAC:
|
||||
assert pydevd_file_utils.normcase(test_dir) == test_dir.lower()
|
||||
assert pydevd_file_utils.get_path_with_real_case(test_dir) == test_dir
|
||||
|
||||
else:
|
||||
# On other platforms, nothing should change
|
||||
# On Linux, nothing should change
|
||||
assert pydevd_file_utils.normcase(test_dir) == test_dir
|
||||
assert pydevd_file_utils.get_path_with_real_case(test_dir) == test_dir
|
||||
|
||||
|
|
@ -351,7 +411,7 @@ def test_zip_paths(tmpdir):
|
|||
# Check that we can deal with the zip path.
|
||||
assert pydevd_file_utils.exists(zipfile_path)
|
||||
abspath, realpath, basename = pydevd_file_utils.get_abs_path_real_path_and_base_from_file(zipfile_path)
|
||||
if IS_WINDOWS:
|
||||
if IS_WINDOWS or IS_MAC:
|
||||
assert abspath == zipfile_path
|
||||
assert basename == zip_basename.lower()
|
||||
else:
|
||||
|
|
@ -431,7 +491,7 @@ def test_source_mapping():
|
|||
assert source_mapping.map_to_client(filename, 12) == (filename, 12, False)
|
||||
|
||||
|
||||
@pytest.mark.skipif(IS_WINDOWS, reason='Linux-only test')
|
||||
@pytest.mark.skipif(IS_WINDOWS, reason='Linux/Mac-only test')
|
||||
def test_mapping_conflict_to_client():
|
||||
import pydevd_file_utils
|
||||
|
||||
|
|
@ -481,7 +541,7 @@ _MAPPING_CONFLICT = [
|
|||
]
|
||||
|
||||
|
||||
@pytest.mark.skipif(IS_WINDOWS, reason='Linux-only test')
|
||||
@pytest.mark.skipif(IS_WINDOWS, reason='Linux/Mac-only test')
|
||||
def test_mapping_conflict_to_server():
|
||||
import pydevd_file_utils
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from _pydevd_bundle.pydevd_constants import IS_WINDOWS
|
||||
from _pydevd_bundle.pydevd_constants import IS_WINDOWS, IS_MAC
|
||||
|
||||
|
||||
def test_in_project_roots_prefix_01(tmpdir):
|
||||
|
|
@ -43,8 +43,16 @@ def test_in_project_roots(tmpdir):
|
|||
|
||||
import os.path
|
||||
import sys
|
||||
assert files_filtering._get_library_roots() == [
|
||||
os.path.normcase(x) + ('\\' if IS_WINDOWS else '/') for x in files_filtering._get_default_library_roots()]
|
||||
|
||||
if IS_WINDOWS:
|
||||
assert files_filtering._get_library_roots() == [
|
||||
os.path.normcase(x) + '\\' for x in files_filtering._get_default_library_roots()]
|
||||
elif IS_MAC:
|
||||
assert files_filtering._get_library_roots() == [
|
||||
x.lower() + '/' for x in files_filtering._get_default_library_roots()]
|
||||
else:
|
||||
assert files_filtering._get_library_roots() == [
|
||||
os.path.normcase(x) + '/' for x in files_filtering._get_default_library_roots()]
|
||||
|
||||
site_packages = tmpdir.mkdir('site-packages')
|
||||
project_dir = tmpdir.mkdir('project')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue