diff --git a/send2trash/plat_osx.py b/send2trash/plat_osx.py index ac6634c..ae3ffc3 100644 --- a/send2trash/plat_osx.py +++ b/send2trash/plat_osx.py @@ -4,53 +4,17 @@ # which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license -from __future__ import unicode_literals +from platform import mac_ver +from sys import version_info -from ctypes import cdll, byref, Structure, c_char, c_char_p -from ctypes.util import find_library - -from .compat import binary_type - -Foundation = cdll.LoadLibrary(find_library("Foundation")) -CoreServices = cdll.LoadLibrary(find_library("CoreServices")) - -GetMacOSStatusCommentString = Foundation.GetMacOSStatusCommentString -GetMacOSStatusCommentString.restype = c_char_p -FSPathMakeRefWithOptions = CoreServices.FSPathMakeRefWithOptions -FSMoveObjectToTrashSync = CoreServices.FSMoveObjectToTrashSync - -kFSPathMakeRefDefaultOptions = 0 -kFSPathMakeRefDoNotFollowLeafSymlink = 0x01 - -kFSFileOperationDefaultOptions = 0 -kFSFileOperationOverwrite = 0x01 -kFSFileOperationSkipSourcePermissionErrors = 0x02 -kFSFileOperationDoNotMoveAcrossVolumes = 0x04 -kFSFileOperationSkipPreflight = 0x08 - - -class FSRef(Structure): - _fields_ = [("hidden", c_char * 80)] - - -def check_op_result(op_result): - if op_result: - msg = GetMacOSStatusCommentString(op_result).decode("utf-8") - raise OSError(msg) - - -def send2trash(paths): - if not isinstance(paths, list): - paths = [paths] - paths = [ - path.encode("utf-8") if not isinstance(path, binary_type) else path - for path in paths - ] - for path in paths: - fp = FSRef() - opts = kFSPathMakeRefDoNotFollowLeafSymlink - op_result = FSPathMakeRefWithOptions(path, opts, byref(fp), None) - check_op_result(op_result) - opts = kFSFileOperationDefaultOptions - op_result = FSMoveObjectToTrashSync(byref(fp), None, opts) - check_op_result(op_result) +# If macOS is 11.0 or newer try to use the pyobjc version to get around #51 +# NOTE: pyobjc only supports python >= 3.6 +if version_info >= (3, 6) and int(mac_ver()[0].split(".", 1)[0]) >= 11: + try: + from .plat_osx_pyobjc import send2trash + except ImportError: + # Try to fall back to ctypes version, although likely problematic still + from .plat_osx_ctypes import send2trash +else: + # Just use the old version otherwise + from .plat_osx_ctypes import send2trash # noqa: F401 diff --git a/send2trash/plat_osx_ctypes.py b/send2trash/plat_osx_ctypes.py new file mode 100644 index 0000000..ac6634c --- /dev/null +++ b/send2trash/plat_osx_ctypes.py @@ -0,0 +1,56 @@ +# Copyright 2017 Virgil Dupras + +# This software is licensed under the "BSD" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/bsd_license + +from __future__ import unicode_literals + +from ctypes import cdll, byref, Structure, c_char, c_char_p +from ctypes.util import find_library + +from .compat import binary_type + +Foundation = cdll.LoadLibrary(find_library("Foundation")) +CoreServices = cdll.LoadLibrary(find_library("CoreServices")) + +GetMacOSStatusCommentString = Foundation.GetMacOSStatusCommentString +GetMacOSStatusCommentString.restype = c_char_p +FSPathMakeRefWithOptions = CoreServices.FSPathMakeRefWithOptions +FSMoveObjectToTrashSync = CoreServices.FSMoveObjectToTrashSync + +kFSPathMakeRefDefaultOptions = 0 +kFSPathMakeRefDoNotFollowLeafSymlink = 0x01 + +kFSFileOperationDefaultOptions = 0 +kFSFileOperationOverwrite = 0x01 +kFSFileOperationSkipSourcePermissionErrors = 0x02 +kFSFileOperationDoNotMoveAcrossVolumes = 0x04 +kFSFileOperationSkipPreflight = 0x08 + + +class FSRef(Structure): + _fields_ = [("hidden", c_char * 80)] + + +def check_op_result(op_result): + if op_result: + msg = GetMacOSStatusCommentString(op_result).decode("utf-8") + raise OSError(msg) + + +def send2trash(paths): + if not isinstance(paths, list): + paths = [paths] + paths = [ + path.encode("utf-8") if not isinstance(path, binary_type) else path + for path in paths + ] + for path in paths: + fp = FSRef() + opts = kFSPathMakeRefDoNotFollowLeafSymlink + op_result = FSPathMakeRefWithOptions(path, opts, byref(fp), None) + check_op_result(op_result) + opts = kFSFileOperationDefaultOptions + op_result = FSMoveObjectToTrashSync(byref(fp), None, opts) + check_op_result(op_result) diff --git a/send2trash/plat_osx_pyobjc.py b/send2trash/plat_osx_pyobjc.py new file mode 100644 index 0000000..e34726e --- /dev/null +++ b/send2trash/plat_osx_pyobjc.py @@ -0,0 +1,29 @@ +# Copyright 2017 Virgil Dupras + +# This software is licensed under the "BSD" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/bsd_license + +from Foundation import NSFileManager, NSURL +from .compat import text_type + + +def check_op_result(op_result): + # First value will be false on failure + if not op_result[0]: + # Error is in third value, localized failure reason matchs ctypes version + raise OSError(op_result[2].localizedFailureReason()) + + +def send2trash(paths): + if not isinstance(paths, list): + paths = [paths] + paths = [ + path.decode("utf-8") if not isinstance(path, text_type) else path + for path in paths + ] + for path in paths: + file_url = NSURL.fileURLWithPath_(path) + fm = NSFileManager.defaultManager() + op_result = fm.trashItemAtURL_resultingItemURL_error_(file_url, None, None) + check_op_result(op_result) diff --git a/tox.ini b/tox.ini index 416e921..6c69397 100644 --- a/tox.ini +++ b/tox.ini @@ -7,6 +7,7 @@ deps = flake8 pytest pywin32; sys_platform == 'win32' + pyobjc-framework-Cocoa; sys_platform == 'darwin' commands = flake8 pytest