diff options
author | Jason R. Coombs <jaraco@jaraco.com> | 2018-01-18 20:54:26 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-01-18 20:54:26 -0500 |
commit | 1942ed24c1d33eff80b638f0aea0d2fef537be02 (patch) | |
tree | 766120d4ebd342ef941c7177a0196ce9d4092141 | |
parent | 80f93ce19f2f0cf37505692458a7576aade7b75f (diff) | |
parent | f3e52e81404d23d14f557cffcd0fe0a25b9c3cfc (diff) | |
download | setuptools-scm-1942ed24c1d33eff80b638f0aea0d2fef537be02.tar.gz |
Merge pull request #207 from pypa/simple-samefile
Use samefile to compare if two paths refer to the same path.
-rw-r--r-- | CHANGELOG.rst | 6 | ||||
-rw-r--r-- | setuptools_scm/git.py | 10 | ||||
-rw-r--r-- | setuptools_scm/utils.py | 57 | ||||
-rw-r--r-- | setuptools_scm/win_py31_compat.py | 214 | ||||
-rw-r--r-- | testing/test_functions.py | 58 |
5 files changed, 228 insertions, 117 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c574737..7908953 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,9 @@ +v1.15.8 +====== + +* #207: Re-use samefile backport as developed in jaraco.windows, + and only use the backport where samefile is not available. + v1.15.7 ====== diff --git a/setuptools_scm/git.py b/setuptools_scm/git.py index 2d5f7fa..7fa1b32 100644 --- a/setuptools_scm/git.py +++ b/setuptools_scm/git.py @@ -1,8 +1,14 @@ -from .utils import do_ex, trace, has_command, _normalized +from .utils import do_ex, trace, has_command from .version import meta from os.path import isfile, join import warnings +try: + from os.path import samefile +except ImportError: + from .win_py31_compat import samefile + + FILES_COMMAND = 'git ls-files' DEFAULT_DESCRIBE = 'git describe --dirty --tags --long --match *.*' @@ -21,7 +27,7 @@ class GitWorkdir(object): if ret: return trace('real root', real_wd) - if _normalized(real_wd) != _normalized(wd): + if not samefile(real_wd, wd): return return cls(real_wd) diff --git a/setuptools_scm/utils.py b/setuptools_scm/utils.py index e338de9..c58ec49 100644 --- a/setuptools_scm/utils.py +++ b/setuptools_scm/utils.py @@ -7,7 +7,6 @@ import sys import shlex import subprocess import os -from os.path import abspath, normcase, realpath import io import platform @@ -108,59 +107,3 @@ def has_command(name): if not res: warnings.warn("%r was not found" % name) return res - - -def _normalized(path): - if IS_WINDOWS: - path = get_windows_long_path_name(path) - return normcase(abspath(realpath(path))) - - -if IS_WINDOWS: - from ctypes import create_unicode_buffer, windll, WinError - from ctypes.wintypes import MAX_PATH, LPCWSTR, LPWSTR, DWORD - - GetLongPathNameW = windll.kernel32.GetLongPathNameW - GetLongPathNameW.argtypes = [LPCWSTR, LPWSTR, DWORD] - GetLongPathNameW.restype = DWORD - - def get_windows_long_path_name(path): - """ - Converts the specified path from short (MS-DOS style) to long form - using the 'GetLongPathNameW' function from Windows API. - - https://msdn.microsoft.com/en-us/library/windows/desktop/aa364980(v=vs.85).aspx - """ - if PY2: - # decode path using filesystem encoding on python2; on python3 - # it is already a unicode string - path = unicode(path, sys.getfilesystemencoding()) # noqa - - pathlen = MAX_PATH + 1 - if DEBUG: - # test reallocation logic - pathlen = 1 - - for _ in range(2): - buf = create_unicode_buffer(pathlen) - retval = GetLongPathNameW(path, buf, pathlen) - - if retval == 0: - # if the function fails for any reason (e.g. file does not - # exist), the return value is zero - raise WinError() - - if retval <= pathlen: - # the function succeeded: the return value is the length of - # the string copied to the buffer - if PY2: - # re-encode to native 'str' type (i.e. bytes) on python2 - return buf.value.encode(sys.getfilesystemencoding()) - return buf.value - - # if the buffer is too small to contain the result, the return - # value is the size of the buffer required to hold the path and - # the terminating NULL char; we retry using a large enough buffer - pathlen = retval - - raise RuntimeError("Failed to get long path name: {!r}".format(path)) diff --git a/setuptools_scm/win_py31_compat.py b/setuptools_scm/win_py31_compat.py new file mode 100644 index 0000000..82a11eb --- /dev/null +++ b/setuptools_scm/win_py31_compat.py @@ -0,0 +1,214 @@ +""" +Backport of os.path.samefile for Python prior to 3.2 +on Windows from jaraco.windows 3.8. + +DON'T EDIT THIS FILE! + +Instead, file tickets and PR's with `jaraco.windows +<https://github.com/jaraco/jaraco.windows>`_ and request +a port to setuptools_scm. +""" + +import os +import nt +import posixpath +import ctypes.wintypes +import sys +import __builtin__ as builtins + + +## +# From jaraco.windows.error + +def format_system_message(errno): + """ + Call FormatMessage with a system error number to retrieve + the descriptive error message. + """ + # first some flags used by FormatMessageW + ALLOCATE_BUFFER = 0x100 + FROM_SYSTEM = 0x1000 + + # Let FormatMessageW allocate the buffer (we'll free it below) + # Also, let it know we want a system error message. + flags = ALLOCATE_BUFFER | FROM_SYSTEM + source = None + message_id = errno + language_id = 0 + result_buffer = ctypes.wintypes.LPWSTR() + buffer_size = 0 + arguments = None + bytes = ctypes.windll.kernel32.FormatMessageW( + flags, + source, + message_id, + language_id, + ctypes.byref(result_buffer), + buffer_size, + arguments, + ) + # note the following will cause an infinite loop if GetLastError + # repeatedly returns an error that cannot be formatted, although + # this should not happen. + handle_nonzero_success(bytes) + message = result_buffer.value + ctypes.windll.kernel32.LocalFree(result_buffer) + return message + + +class WindowsError(builtins.WindowsError): + """ + More info about errors at + http://msdn.microsoft.com/en-us/library/ms681381(VS.85).aspx + """ + + def __init__(self, value=None): + if value is None: + value = ctypes.windll.kernel32.GetLastError() + strerror = format_system_message(value) + if sys.version_info > (3, 3): + args = 0, strerror, None, value + else: + args = value, strerror + super(WindowsError, self).__init__(*args) + + @property + def message(self): + return self.strerror + + @property + def code(self): + return self.winerror + + def __str__(self): + return self.message + + def __repr__(self): + return '{self.__class__.__name__}({self.winerror})'.format(**vars()) + + +def handle_nonzero_success(result): + if result == 0: + raise WindowsError() + + +## +# From jaraco.windows.api.filesystem + +FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000 +FILE_FLAG_BACKUP_SEMANTICS = 0x2000000 +OPEN_EXISTING = 3 +FILE_ATTRIBUTE_NORMAL = 0x80 +FILE_READ_ATTRIBUTES = 0x80 +INVALID_HANDLE_VALUE = ctypes.wintypes.HANDLE(-1).value + + +class BY_HANDLE_FILE_INFORMATION(ctypes.Structure): + _fields_ = [ + ('file_attributes', ctypes.wintypes.DWORD), + ('creation_time', ctypes.wintypes.FILETIME), + ('last_access_time', ctypes.wintypes.FILETIME), + ('last_write_time', ctypes.wintypes.FILETIME), + ('volume_serial_number', ctypes.wintypes.DWORD), + ('file_size_high', ctypes.wintypes.DWORD), + ('file_size_low', ctypes.wintypes.DWORD), + ('number_of_links', ctypes.wintypes.DWORD), + ('file_index_high', ctypes.wintypes.DWORD), + ('file_index_low', ctypes.wintypes.DWORD), + ] + + @property + def file_size(self): + return (self.file_size_high << 32) + self.file_size_low + + @property + def file_index(self): + return (self.file_index_high << 32) + self.file_index_low + + +class SECURITY_ATTRIBUTES(ctypes.Structure): + _fields_ = ( + ('length', ctypes.wintypes.DWORD), + ('p_security_descriptor', ctypes.wintypes.LPVOID), + ('inherit_handle', ctypes.wintypes.BOOLEAN), + ) + + +LPSECURITY_ATTRIBUTES = ctypes.POINTER(SECURITY_ATTRIBUTES) + + +CreateFile = ctypes.windll.kernel32.CreateFileW +CreateFile.argtypes = ( + ctypes.wintypes.LPWSTR, + ctypes.wintypes.DWORD, + ctypes.wintypes.DWORD, + LPSECURITY_ATTRIBUTES, + ctypes.wintypes.DWORD, + ctypes.wintypes.DWORD, + ctypes.wintypes.HANDLE, +) +CreateFile.restype = ctypes.wintypes.HANDLE + +GetFileInformationByHandle = ctypes.windll.kernel32.GetFileInformationByHandle +GetFileInformationByHandle.restype = ctypes.wintypes.BOOL +GetFileInformationByHandle.argtypes = ( + ctypes.wintypes.HANDLE, + ctypes.POINTER(BY_HANDLE_FILE_INFORMATION), +) + + +## +# From jaraco.windows.filesystem + +def compat_stat(path): + """ + Generate stat as found on Python 3.2 and later. + """ + stat = os.stat(path) + info = get_file_info(path) + # rewrite st_ino, st_dev, and st_nlink based on file info + return nt.stat_result( + (stat.st_mode,) + + (info.file_index, info.volume_serial_number, info.number_of_links) + + stat[4:] + ) + + +def samefile(f1, f2): + """ + Backport of samefile from Python 3.2 with support for Windows. + """ + return posixpath.samestat(compat_stat(f1), compat_stat(f2)) + + +def get_file_info(path): + # open the file the same way CPython does in posixmodule.c + desired_access = FILE_READ_ATTRIBUTES + share_mode = 0 + security_attributes = None + creation_disposition = OPEN_EXISTING + flags_and_attributes = ( + FILE_ATTRIBUTE_NORMAL | + FILE_FLAG_BACKUP_SEMANTICS | + FILE_FLAG_OPEN_REPARSE_POINT + ) + template_file = None + + handle = CreateFile( + path, + desired_access, + share_mode, + security_attributes, + creation_disposition, + flags_and_attributes, + template_file, + ) + + if handle == INVALID_HANDLE_VALUE: + raise WindowsError() + + info = BY_HANDLE_FILE_INFORMATION() + res = GetFileInformationByHandle(handle, info) + handle_nonzero_success(res) + + return info diff --git a/testing/test_functions.py b/testing/test_functions.py index 8fbf750..c3cc86e 100644 --- a/testing/test_functions.py +++ b/testing/test_functions.py @@ -1,11 +1,9 @@ import pytest -import py import sys import pkg_resources from setuptools_scm import dump_version, get_version, PRETEND_KEY from setuptools_scm.version import guess_next_version, meta, format_version from setuptools_scm.utils import has_command -import subprocess PY3 = sys.version_info > (2,) @@ -75,59 +73,3 @@ def test_has_command(recwarn): assert not has_command('yadayada_setuptools_aint_ne') msg = recwarn.pop() assert 'yadayada' in str(msg.message) - - -def _get_windows_short_path(path): - """ Call a temporary batch file that expands the first argument so that - it contains short names only. - Return a py._path.local.LocalPath instance. - - For info on Windows batch parameters: - https://www.microsoft.com/resources/documentation/windows/xp/all/proddocs/en-us/percent.mspx?mfr=true - """ - tmpdir = py.path.local.mkdtemp() - try: - batch_file = tmpdir.join("shortpathname.bat") - batch_file.write("@echo %~s1") - out = subprocess.check_output([str(batch_file), str(path)]) - if PY3: - out = out.decode(sys.getfilesystemencoding()) - finally: - tmpdir.remove() - return py.path.local(out.strip()) - - -@pytest.mark.skipif(sys.platform != 'win32', - reason="this test is only valid on windows") -def test_get_windows_long_path_name(tmpdir): - from setuptools_scm.utils import get_windows_long_path_name - - # 8.3 names are limited to max 8 characters, plus optionally a period - # and three further characters; so here we use longer names - file_a = tmpdir.ensure("long_name_a.txt") - file_b = tmpdir.ensure("long_name_b.txt") - dir_c = tmpdir.ensure("long_name_c", dir=True) - short_file_a = _get_windows_short_path(file_a) - short_file_b = _get_windows_short_path(file_b) - short_dir_c = _get_windows_short_path(dir_c) - - # shortened names contain the first six characters (case insensitive), - # followed by a tilde character and an incremental number that - # distinguishes files with the same first six letters and extension - assert short_file_a.basename == "LONG_N~1.TXT" - assert short_file_b.basename == "LONG_N~2.TXT" - assert short_dir_c.basename == "LONG_N~1" - - long_name_a = get_windows_long_path_name(str(short_file_a)) - long_name_b = get_windows_long_path_name(str(short_file_b)) - long_name_c = get_windows_long_path_name(str(short_dir_c)) - assert long_name_a.endswith("long_name_a.txt") - assert long_name_b.endswith("long_name_b.txt") - assert long_name_c.endswith("long_name_c") - - # check ctypes.WinError() with no arg shows the last error message, e.g. - # when input path doesn't exist. Note, WinError is not itself a subclass - # of BaseException; it's a function returning an instance of OSError - with pytest.raises(OSError) as excinfo: - get_windows_long_path_name("unexistent_file_name") - assert 'The system cannot find the file specified' in str(excinfo) |