mirror of
https://github.com/arsenetar/send2trash.git
synced 2026-03-12 02:41:39 +00:00
Compare commits
16 Commits
af0c1ba704
...
1.8.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
484913ba0f
|
|||
|
d249f0106b
|
|||
|
94e1ec007a
|
|||
|
84c220cbd9
|
|||
|
6612545110
|
|||
|
d52b4f206c
|
|||
|
33171dde82
|
|||
| 077598d2ce | |||
|
|
436686bf0f | ||
|
23ce7b8c16
|
|||
|
9b0d5796c1
|
|||
|
c8bcaea1e8
|
|||
| 530e9b4bc6 | |||
|
10c7693d11
|
|||
|
356509120b
|
|||
|
f9fcdb8d8c
|
29
CHANGES.rst
29
CHANGES.rst
@@ -1,6 +1,35 @@
|
|||||||
Changes
|
Changes
|
||||||
=======
|
=======
|
||||||
|
|
||||||
|
Version 1.8.0 -- 2021/08/08
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
* Add compatibility with pathlib paths (#49)
|
||||||
|
* Fix thread compatibility of modern windows implementation (#59)
|
||||||
|
* Fix handling of UNC names in legacy windows implementation (#57)
|
||||||
|
|
||||||
|
Version 1.7.1 -- 2021/06/21
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
* Release stable version with changes from last 3 releases
|
||||||
|
* Fix handling of UNC names (#57)
|
||||||
|
|
||||||
|
Version 1.7.0a1 -- 2021/05/14
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
* Changed conditional for when to try to use pyobjc version (#51)
|
||||||
|
|
||||||
|
Version 1.7.0a0 -- 2021/04/20
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
* Add console_script entry point (#50)
|
||||||
|
* Increased python CI versions (#52, #54)
|
||||||
|
* Fix minor issue in setup.py (#53)
|
||||||
|
* Fix issue with windows tests importing modules on non-windows (#55)
|
||||||
|
* Unit test cleanups, rewrites, and flake8 cleanups
|
||||||
|
* Windows: Fix legacy windows platform for multi-byte unicode and add tests
|
||||||
|
* macOS: Add alternative pyobjc version to potentially improve compatibility (#51)
|
||||||
|
|
||||||
Version 1.6.0b1 -- 2020/06/18
|
Version 1.6.0b1 -- 2020/06/18
|
||||||
-----------------------------
|
-----------------------------
|
||||||
|
|
||||||
|
|||||||
16
README.rst
16
README.rst
@@ -3,11 +3,11 @@ Send2Trash -- Send files to trash on all platforms
|
|||||||
==================================================
|
==================================================
|
||||||
|
|
||||||
Send2Trash is a small package that sends files to the Trash (or Recycle Bin) *natively* and on
|
Send2Trash is a small package that sends files to the Trash (or Recycle Bin) *natively* and on
|
||||||
*all platforms*. On OS X, it uses native ``FSMoveObjectToTrashSync`` Cocoa calls. On Windows, it
|
*all platforms*. On OS X, it uses native ``FSMoveObjectToTrashSync`` Cocoa calls or can use pyobjc
|
||||||
uses native ``IFileOperation`` call if on Vista or newer and pywin32 is installed or falls back
|
with NSFileManager. On Windows, it uses native ``IFileOperation`` call if on Vista or newer and
|
||||||
to ``SHFileOperation`` calls. On other platforms, if `PyGObject`_ and `GIO`_ are available, it
|
pywin32 is installed or falls back to ``SHFileOperation`` calls. On other platforms, if `PyGObject`_
|
||||||
will use this. Otherwise, it will fallback to its own implementation of the `trash specifications
|
and `GIO`_ are available, it will use this. Otherwise, it will fallback to its own implementation of
|
||||||
from freedesktop.org`_.
|
the `trash specifications from freedesktop.org`_.
|
||||||
|
|
||||||
``ctypes`` is used to access native libraries, so no compilation is necessary.
|
``ctypes`` is used to access native libraries, so no compilation is necessary.
|
||||||
|
|
||||||
@@ -22,10 +22,14 @@ issues and fixes would be most appreciated.
|
|||||||
Installation
|
Installation
|
||||||
------------
|
------------
|
||||||
|
|
||||||
You can download it with pip::
|
You can download it with pip:
|
||||||
|
|
||||||
python -m pip install -U send2trash
|
python -m pip install -U send2trash
|
||||||
|
|
||||||
|
To install with pywin32 or pyobjc required specify the extra `nativeLib`:
|
||||||
|
|
||||||
|
python -m pip install -U send2trash[nativeLib]
|
||||||
|
|
||||||
or you can download the source from http://github.com/arsenetar/send2trash and install it with::
|
or you can download the source from http://github.com/arsenetar/send2trash and install it with::
|
||||||
|
|
||||||
>>> python setup.py install
|
>>> python setup.py install
|
||||||
|
|||||||
@@ -6,11 +6,11 @@
|
|||||||
|
|
||||||
from gi.repository import GObject, Gio
|
from gi.repository import GObject, Gio
|
||||||
from .exceptions import TrashPermissionError
|
from .exceptions import TrashPermissionError
|
||||||
|
from .util import preprocess_paths
|
||||||
|
|
||||||
|
|
||||||
def send2trash(paths):
|
def send2trash(paths):
|
||||||
if not isinstance(paths, list):
|
paths = preprocess_paths(paths)
|
||||||
paths = [paths]
|
|
||||||
for path in paths:
|
for path in paths:
|
||||||
try:
|
try:
|
||||||
f = Gio.File.new_for_path(path)
|
f = Gio.File.new_for_path(path)
|
||||||
|
|||||||
@@ -4,53 +4,17 @@
|
|||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/bsd_license
|
# 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
|
# NOTE: version of pyobjc only supports python >= 3.6 and 10.9+
|
||||||
from ctypes.util import find_library
|
macos_ver = tuple(int(part) for part in mac_ver()[0].split("."))
|
||||||
|
if version_info >= (3, 6) and macos_ver >= (10, 9):
|
||||||
from .compat import binary_type
|
try:
|
||||||
|
from .plat_osx_pyobjc import send2trash
|
||||||
Foundation = cdll.LoadLibrary(find_library("Foundation"))
|
except ImportError:
|
||||||
CoreServices = cdll.LoadLibrary(find_library("CoreServices"))
|
# Try to fall back to ctypes version, although likely problematic still
|
||||||
|
from .plat_osx_ctypes import send2trash
|
||||||
GetMacOSStatusCommentString = Foundation.GetMacOSStatusCommentString
|
else:
|
||||||
GetMacOSStatusCommentString.restype = c_char_p
|
# Just use the old version otherwise
|
||||||
FSPathMakeRefWithOptions = CoreServices.FSPathMakeRefWithOptions
|
from .plat_osx_ctypes import send2trash # noqa: F401
|
||||||
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)
|
|
||||||
|
|||||||
56
send2trash/plat_osx_ctypes.py
Normal file
56
send2trash/plat_osx_ctypes.py
Normal file
@@ -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
|
||||||
|
from .util import preprocess_paths
|
||||||
|
|
||||||
|
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):
|
||||||
|
paths = preprocess_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)
|
||||||
29
send2trash/plat_osx_pyobjc.py
Normal file
29
send2trash/plat_osx_pyobjc.py
Normal file
@@ -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
|
||||||
|
from .util import preprocess_paths
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
||||||
|
paths = preprocess_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)
|
||||||
@@ -30,6 +30,7 @@ except ImportError:
|
|||||||
from urllib import quote
|
from urllib import quote
|
||||||
|
|
||||||
from .compat import text_type, environb
|
from .compat import text_type, environb
|
||||||
|
from .util import preprocess_paths
|
||||||
from .exceptions import TrashPermissionError
|
from .exceptions import TrashPermissionError
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -172,8 +173,7 @@ def get_dev(path):
|
|||||||
|
|
||||||
|
|
||||||
def send2trash(paths):
|
def send2trash(paths):
|
||||||
if not isinstance(paths, list):
|
paths = preprocess_paths(paths)
|
||||||
paths = [paths]
|
|
||||||
for path in paths:
|
for path in paths:
|
||||||
if isinstance(path, text_type):
|
if isinstance(path, text_type):
|
||||||
path_b = fsencode(path)
|
path_b = fsencode(path)
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import os.path as op
|
import os.path as op
|
||||||
from .compat import text_type
|
from .compat import text_type
|
||||||
|
from .util import preprocess_paths
|
||||||
|
|
||||||
from ctypes import (
|
from ctypes import (
|
||||||
windll,
|
windll,
|
||||||
Structure,
|
Structure,
|
||||||
@@ -51,23 +53,57 @@ FOF_ALLOWUNDO = 64
|
|||||||
FOF_NOERRORUI = 1024
|
FOF_NOERRORUI = 1024
|
||||||
|
|
||||||
|
|
||||||
|
def prefix_and_path(path):
|
||||||
|
r"""Guess the long-path prefix based on the kind of *path*.
|
||||||
|
Local paths (C:\folder\file.ext) and UNC names (\\server\folder\file.ext)
|
||||||
|
are handled.
|
||||||
|
|
||||||
|
Return a tuple of the long-path prefix and the prefixed path.
|
||||||
|
"""
|
||||||
|
prefix, long_path = "\\\\?\\", path
|
||||||
|
|
||||||
|
if not path.startswith(prefix):
|
||||||
|
if path.startswith("\\\\"):
|
||||||
|
# Likely a UNC name
|
||||||
|
prefix = "\\\\?\\UNC"
|
||||||
|
long_path = prefix + path[1:]
|
||||||
|
else:
|
||||||
|
# Likely a local path
|
||||||
|
long_path = prefix + path
|
||||||
|
elif path.startswith(prefix + "UNC\\"):
|
||||||
|
# UNC name with long-path prefix
|
||||||
|
prefix = "\\\\?\\UNC"
|
||||||
|
|
||||||
|
return prefix, long_path
|
||||||
|
|
||||||
|
|
||||||
|
def get_awaited_path_from_prefix(prefix, path):
|
||||||
|
"""Guess the correct path to pass to the SHFileOperationW() call.
|
||||||
|
The long-path prefix must be removed, so we should take care of
|
||||||
|
different long-path prefixes.
|
||||||
|
"""
|
||||||
|
if prefix == "\\\\?\\UNC":
|
||||||
|
# We need to prepend a backslash for UNC names, as it was removed
|
||||||
|
# in prefix_and_path().
|
||||||
|
return "\\" + path[len(prefix) :]
|
||||||
|
return path[len(prefix) :]
|
||||||
|
|
||||||
|
|
||||||
def get_short_path_name(long_name):
|
def get_short_path_name(long_name):
|
||||||
if not long_name.startswith("\\\\?\\"):
|
prefix, long_path = prefix_and_path(long_name)
|
||||||
long_name = "\\\\?\\" + long_name
|
buf_size = GetShortPathNameW(long_path, None, 0)
|
||||||
buf_size = GetShortPathNameW(long_name, None, 0)
|
|
||||||
# FIX: https://github.com/hsoft/send2trash/issues/31
|
# FIX: https://github.com/hsoft/send2trash/issues/31
|
||||||
# If buffer size is zero, an error has occurred.
|
# If buffer size is zero, an error has occurred.
|
||||||
if not buf_size:
|
if not buf_size:
|
||||||
err_no = GetLastError()
|
err_no = GetLastError()
|
||||||
raise WindowsError(err_no, FormatError(err_no), long_name[4:])
|
raise WindowsError(err_no, FormatError(err_no), long_path)
|
||||||
output = create_unicode_buffer(buf_size)
|
output = create_unicode_buffer(buf_size)
|
||||||
GetShortPathNameW(long_name, output, buf_size)
|
GetShortPathNameW(long_path, output, buf_size)
|
||||||
return output.value[4:] # Remove '\\?\' for SHFileOperationW
|
return get_awaited_path_from_prefix(prefix, output.value)
|
||||||
|
|
||||||
|
|
||||||
def send2trash(paths):
|
def send2trash(paths):
|
||||||
if not isinstance(paths, list):
|
paths = preprocess_paths(paths)
|
||||||
paths = [paths]
|
|
||||||
# convert data type
|
# convert data type
|
||||||
paths = [
|
paths = [
|
||||||
text_type(path, "mbcs") if not isinstance(path, text_type) else path
|
text_type(path, "mbcs") if not isinstance(path, text_type) else path
|
||||||
@@ -77,8 +113,6 @@ def send2trash(paths):
|
|||||||
paths = [op.abspath(path) if not op.isabs(path) else path for path in paths]
|
paths = [op.abspath(path) if not op.isabs(path) else path for path in paths]
|
||||||
# get short path to handle path length issues
|
# get short path to handle path length issues
|
||||||
paths = [get_short_path_name(path) for path in paths]
|
paths = [get_short_path_name(path) for path in paths]
|
||||||
# convert to a single string of null terminated paths
|
|
||||||
paths = "\0".join(paths)
|
|
||||||
fileop = SHFILEOPSTRUCTW()
|
fileop = SHFILEOPSTRUCTW()
|
||||||
fileop.hwnd = 0
|
fileop.hwnd = 0
|
||||||
fileop.wFunc = FO_DELETE
|
fileop.wFunc = FO_DELETE
|
||||||
@@ -93,7 +127,15 @@ def send2trash(paths):
|
|||||||
# NOTE: based on how python allocates memory for these types they should
|
# NOTE: based on how python allocates memory for these types they should
|
||||||
# always be zero, if this is ever not true we can go back to explicitly
|
# always be zero, if this is ever not true we can go back to explicitly
|
||||||
# setting the last two characters to null using buffer[index] = '\0'.
|
# setting the last two characters to null using buffer[index] = '\0'.
|
||||||
buffer = create_unicode_buffer(paths, len(paths) + 2)
|
# Additional note on another issue here, unicode_buffer expects length in
|
||||||
|
# bytes essentially, so having multi-byte characters causes issues if just
|
||||||
|
# passing pythons string length. Instead of dealing with this difference we
|
||||||
|
# just create a buffer then a new one with an extra null. Since the non-length
|
||||||
|
# specified version apparently stops after the first null, join with a space first.
|
||||||
|
buffer = create_unicode_buffer(" ".join(paths))
|
||||||
|
# convert to a single string of null terminated paths
|
||||||
|
path_string = "\0".join(paths)
|
||||||
|
buffer = create_unicode_buffer(path_string, len(buffer) + 1)
|
||||||
fileop.pFrom = LPCWSTR(addressof(buffer))
|
fileop.pFrom = LPCWSTR(addressof(buffer))
|
||||||
fileop.pTo = None
|
fileop.pTo = None
|
||||||
fileop.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT
|
fileop.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import os.path as op
|
import os.path as op
|
||||||
from .compat import text_type
|
from .compat import text_type
|
||||||
|
from .util import preprocess_paths
|
||||||
from platform import version
|
from platform import version
|
||||||
import pythoncom
|
import pythoncom
|
||||||
import pywintypes
|
import pywintypes
|
||||||
@@ -15,8 +16,7 @@ from .IFileOperationProgressSink import CreateSink
|
|||||||
|
|
||||||
|
|
||||||
def send2trash(paths):
|
def send2trash(paths):
|
||||||
if not isinstance(paths, list):
|
paths = preprocess_paths(paths)
|
||||||
paths = [paths]
|
|
||||||
# convert data type
|
# convert data type
|
||||||
paths = [
|
paths = [
|
||||||
text_type(path, "mbcs") if not isinstance(path, text_type) else path
|
text_type(path, "mbcs") if not isinstance(path, text_type) else path
|
||||||
@@ -26,6 +26,8 @@ def send2trash(paths):
|
|||||||
paths = [op.abspath(path) if not op.isabs(path) else path for path in paths]
|
paths = [op.abspath(path) if not op.isabs(path) else path for path in paths]
|
||||||
# remove the leading \\?\ if present
|
# remove the leading \\?\ if present
|
||||||
paths = [path[4:] if path.startswith("\\\\?\\") else path for path in paths]
|
paths = [path[4:] if path.startswith("\\\\?\\") else path for path in paths]
|
||||||
|
# Need to initialize the com before using
|
||||||
|
pythoncom.CoInitialize()
|
||||||
# create instance of file operation object
|
# create instance of file operation object
|
||||||
fileop = pythoncom.CoCreateInstance(
|
fileop = pythoncom.CoCreateInstance(
|
||||||
shell.CLSID_FileOperation, None, pythoncom.CLSCTX_ALL, shell.IID_IFileOperation,
|
shell.CLSID_FileOperation, None, pythoncom.CLSCTX_ALL, shell.IID_IFileOperation,
|
||||||
@@ -65,3 +67,6 @@ def send2trash(paths):
|
|||||||
# convert to standard OS error, allows other code to get a
|
# convert to standard OS error, allows other code to get a
|
||||||
# normal errno
|
# normal errno
|
||||||
raise OSError(None, error.strerror, path, error.hresult)
|
raise OSError(None, error.strerror, path, error.hresult)
|
||||||
|
finally:
|
||||||
|
# Need to make sure we call this once fore every init
|
||||||
|
pythoncom.CoUninitialize()
|
||||||
|
|||||||
16
send2trash/util.py
Normal file
16
send2trash/util.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# encoding: utf-8
|
||||||
|
# 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
|
||||||
|
|
||||||
|
|
||||||
|
def preprocess_paths(paths):
|
||||||
|
if not isinstance(paths, list):
|
||||||
|
paths = [paths]
|
||||||
|
# Convert items such as pathlib paths to strings
|
||||||
|
paths = [
|
||||||
|
path.__fspath__() if hasattr(path, "__fspath__") else path for path in paths
|
||||||
|
]
|
||||||
|
return paths
|
||||||
12
setup.py
12
setup.py
@@ -24,7 +24,7 @@ with open("README.rst", "rt") as f1, open("CHANGES.rst", "rt") as f2:
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="Send2Trash",
|
name="Send2Trash",
|
||||||
version="1.6.0b1",
|
version="1.8.0",
|
||||||
author="Andrew Senetar",
|
author="Andrew Senetar",
|
||||||
author_email="arsenetar@voltaicideas.net",
|
author_email="arsenetar@voltaicideas.net",
|
||||||
packages=["send2trash"],
|
packages=["send2trash"],
|
||||||
@@ -34,8 +34,16 @@ setup(
|
|||||||
license="BSD License",
|
license="BSD License",
|
||||||
description="Send file to trash natively under Mac OS X, Windows and Linux.",
|
description="Send file to trash natively under Mac OS X, Windows and Linux.",
|
||||||
long_description=LONG_DESCRIPTION,
|
long_description=LONG_DESCRIPTION,
|
||||||
|
long_description_content_type="text/x-rst",
|
||||||
classifiers=CLASSIFIERS,
|
classifiers=CLASSIFIERS,
|
||||||
extras_require={"win32": ["pywin32"]},
|
extras_require={
|
||||||
|
"win32": ['pywin32; sys_platform == "win32"'],
|
||||||
|
"objc": ['pyobjc-framework-Cocoa; sys_platform == "darwin"'],
|
||||||
|
"nativeLib": [
|
||||||
|
'pywin32; sys_platform == "win32"',
|
||||||
|
'pyobjc-framework-Cocoa; sys_platform == "darwin"',
|
||||||
|
],
|
||||||
|
},
|
||||||
project_urls={"Bug Reports": "https://github.com/arsenetar/send2trash/issues"},
|
project_urls={"Bug Reports": "https://github.com/arsenetar/send2trash/issues"},
|
||||||
entry_points={"console_scripts": ["send2trash=send2trash.__main__:main"]},
|
entry_points={"console_scripts": ["send2trash=send2trash.__main__:main"]},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ def testfile():
|
|||||||
dir=op.expanduser("~"), prefix="send2trash_test", delete=False
|
dir=op.expanduser("~"), prefix="send2trash_test", delete=False
|
||||||
)
|
)
|
||||||
file.close()
|
file.close()
|
||||||
|
assert op.exists(file.name) is True
|
||||||
yield file
|
yield file
|
||||||
# Cleanup trash files on supported platforms
|
# Cleanup trash files on supported platforms
|
||||||
if sys.platform != "win32":
|
if sys.platform != "win32":
|
||||||
@@ -57,6 +58,7 @@ def testfiles():
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
[file.close() for file in files]
|
[file.close() for file in files]
|
||||||
|
assert all([op.exists(file.name) for file in files]) is True
|
||||||
yield files
|
yield files
|
||||||
filenames = [op.basename(file.name) for file in files]
|
filenames = [op.basename(file.name) for file in files]
|
||||||
[os.remove(op.join(HOMETRASH, "files", filename)) for filename in filenames]
|
[os.remove(op.join(HOMETRASH, "files", filename)) for filename in filenames]
|
||||||
@@ -93,6 +95,7 @@ def testUnicodefile():
|
|||||||
name = u"send2trash_tést1"
|
name = u"send2trash_tést1"
|
||||||
file = op.join(op.expanduser(b"~"), name.encode("utf-8"))
|
file = op.join(op.expanduser(b"~"), name.encode("utf-8"))
|
||||||
touch(file)
|
touch(file)
|
||||||
|
assert op.exists(file) is True
|
||||||
yield file
|
yield file
|
||||||
# Cleanup trash files on supported platforms
|
# Cleanup trash files on supported platforms
|
||||||
if sys.platform != "win32":
|
if sys.platform != "win32":
|
||||||
@@ -158,6 +161,7 @@ def testExtVol():
|
|||||||
fileName = "test.txt"
|
fileName = "test.txt"
|
||||||
filePath = op.join(volume.trashTopdir, fileName)
|
filePath = op.join(volume.trashTopdir, fileName)
|
||||||
touch(filePath)
|
touch(filePath)
|
||||||
|
assert op.exists(filePath) is True
|
||||||
yield volume, fileName, filePath
|
yield volume, fileName, filePath
|
||||||
volume.cleanup()
|
volume.cleanup()
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ def _create_tree(path):
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def testdir(tmp_path):
|
def testdir(tmp_path):
|
||||||
dirname = "\\\\?\\" + str(tmp_path)
|
dirname = "\\\\?\\" + str(tmp_path)
|
||||||
|
assert op.exists(dirname) is True
|
||||||
yield dirname
|
yield dirname
|
||||||
shutil.rmtree(dirname, ignore_errors=True)
|
shutil.rmtree(dirname, ignore_errors=True)
|
||||||
|
|
||||||
@@ -34,6 +35,7 @@ def testdir(tmp_path):
|
|||||||
def testfile(testdir):
|
def testfile(testdir):
|
||||||
file = op.join(testdir, "testfile.txt")
|
file = op.join(testdir, "testfile.txt")
|
||||||
_create_tree(file)
|
_create_tree(file)
|
||||||
|
assert op.exists(file) is True
|
||||||
yield file
|
yield file
|
||||||
# Note dir will cleanup the file
|
# Note dir will cleanup the file
|
||||||
|
|
||||||
@@ -42,6 +44,7 @@ def testfile(testdir):
|
|||||||
def testfiles(testdir):
|
def testfiles(testdir):
|
||||||
files = [op.join(testdir, "testfile{}.txt".format(index)) for index in range(10)]
|
files = [op.join(testdir, "testfile{}.txt".format(index)) for index in range(10)]
|
||||||
[_create_tree(file) for file in files]
|
[_create_tree(file) for file in files]
|
||||||
|
assert all([op.exists(file) for file in files]) is True
|
||||||
yield files
|
yield files
|
||||||
# Note dir will cleanup the files
|
# Note dir will cleanup the files
|
||||||
|
|
||||||
@@ -66,6 +69,19 @@ def _file_not_found(dir, fcn):
|
|||||||
pytest.raises(OSError, fcn, file)
|
pytest.raises(OSError, fcn, file)
|
||||||
|
|
||||||
|
|
||||||
|
def _multi_byte_unicode(dir, fcn):
|
||||||
|
single_file = op.join(dir, "😇.txt")
|
||||||
|
_create_tree(single_file)
|
||||||
|
assert op.exists(single_file) is True
|
||||||
|
fcn(single_file)
|
||||||
|
assert op.exists(single_file) is False
|
||||||
|
files = [op.join(dir, "😇{}.txt".format(index)) for index in range(10)]
|
||||||
|
[_create_tree(file) for file in files]
|
||||||
|
assert all([op.exists(file) for file in files]) is True
|
||||||
|
fcn(files)
|
||||||
|
assert any([op.exists(file) for file in files]) is False
|
||||||
|
|
||||||
|
|
||||||
def test_trash_folder(testdir):
|
def test_trash_folder(testdir):
|
||||||
_trash_folder(testdir, s2t)
|
_trash_folder(testdir, s2t)
|
||||||
|
|
||||||
@@ -98,6 +114,10 @@ def test_file_not_found_modern(testdir):
|
|||||||
_file_not_found(testdir, s2t_modern)
|
_file_not_found(testdir, s2t_modern)
|
||||||
|
|
||||||
|
|
||||||
|
def test_multi_byte_unicode_modern(testdir):
|
||||||
|
_multi_byte_unicode(testdir, s2t_modern)
|
||||||
|
|
||||||
|
|
||||||
def test_trash_folder_legacy(testdir):
|
def test_trash_folder_legacy(testdir):
|
||||||
_trash_folder(testdir, s2t_legacy)
|
_trash_folder(testdir, s2t_legacy)
|
||||||
|
|
||||||
@@ -114,6 +134,10 @@ def test_file_not_found_legacy(testdir):
|
|||||||
_file_not_found(testdir, s2t_legacy)
|
_file_not_found(testdir, s2t_legacy)
|
||||||
|
|
||||||
|
|
||||||
|
def test_multi_byte_unicode_legacy(testdir):
|
||||||
|
_multi_byte_unicode(testdir, s2t_legacy)
|
||||||
|
|
||||||
|
|
||||||
# Long path tests
|
# Long path tests
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def longdir(tmp_path):
|
def longdir(tmp_path):
|
||||||
@@ -129,6 +153,7 @@ def longfile(longdir):
|
|||||||
path = op.join(longdir, name + "{}.txt")
|
path = op.join(longdir, name + "{}.txt")
|
||||||
file = path.format("")
|
file = path.format("")
|
||||||
_create_tree(file)
|
_create_tree(file)
|
||||||
|
assert op.exists(file) is True
|
||||||
yield file
|
yield file
|
||||||
|
|
||||||
|
|
||||||
@@ -138,11 +163,14 @@ def longfiles(longdir):
|
|||||||
path = op.join(longdir, name + "{}.txt")
|
path = op.join(longdir, name + "{}.txt")
|
||||||
files = [path.format(index) for index in range(10)]
|
files = [path.format(index) for index in range(10)]
|
||||||
[_create_tree(file) for file in files]
|
[_create_tree(file) for file in files]
|
||||||
|
assert all([op.exists(file) for file in files]) is True
|
||||||
yield files
|
yield files
|
||||||
|
|
||||||
|
|
||||||
# NOTE: both legacy and modern test "pass" on windows, but actually are not moving files to the
|
# NOTE: both legacy and modern test "pass" on windows, however sometimes with the same path
|
||||||
# recycle bin, this was tested on latest windows 10, thought to have worked previously
|
# they do not actually recycle files but delete them. Noticed this when testing with the
|
||||||
|
# recycle bin open, noticed later tests actually worked, modern version can actually detect
|
||||||
|
# when this happens but not stop it at this moment, and we need a way to verify it when testing.
|
||||||
def test_trash_long_file_modern(longfile):
|
def test_trash_long_file_modern(longfile):
|
||||||
_trash_file(longfile, s2t_modern)
|
_trash_file(longfile, s2t_modern)
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ def file():
|
|||||||
dir=op.expanduser("~"), prefix="send2trash_test", delete=False
|
dir=op.expanduser("~"), prefix="send2trash_test", delete=False
|
||||||
)
|
)
|
||||||
file.close()
|
file.close()
|
||||||
|
# Verify file was actually created
|
||||||
|
assert op.exists(file.name) is True
|
||||||
yield file.name
|
yield file.name
|
||||||
# Cleanup trash files on supported platforms
|
# Cleanup trash files on supported platforms
|
||||||
if sys.platform != "win32":
|
if sys.platform != "win32":
|
||||||
|
|||||||
Reference in New Issue
Block a user