1
0
mirror of https://github.com/arsenetar/send2trash.git synced 2026-01-25 16:11:39 +00:00

25 Commits

Author SHA1 Message Date
2aa834be94 Update version & changelog for 1.8.1b0 2021-08-20 22:42:04 -05:00
5e4517aa53 Add fallback to HOMETRASH on plat_other
In the case where os.path.ismount() does not detect a mount and os.rename errors
fallback to HOMETRASH.  This covers several situations where continuing with
the identified trash location is incorrect due to complex mounts.

Close #26, #41, #63.
2021-08-20 22:30:51 -05:00
62849fba0b Remove Python 3.4 2021-08-19 21:01:55 -05:00
74f2dff57b Add exception handling to file cleanup
- Surpress errors caused by long file cleanup in older python environments
2021-08-19 20:46:02 -05:00
922fc0342a Update tox config 2021-08-19 20:46:02 -05:00
1d1b8755a9 Create codeql-analysis.yml
Test out codeql
2021-08-18 02:26:10 -05:00
d0e4890a4d Black format updates with correct line length 2021-08-17 18:58:11 -05:00
24079e245c Update tox.ini, flake8 ignore fewer errors 2021-08-17 18:54:52 -05:00
24b38e4ffe Update to use pyproject.toml & setup.cfg 2021-08-17 18:53:56 -05:00
69a82a5162 Dropping duplicate runs from travis CI
- Drop the duplicate runs from travis that github actions now cover.
- Add additional ppc64le entries for 3.7, 3.8, 3.9
2021-08-17 01:51:43 -05:00
2b3f9891c2 Temporarily comment out macOS due to failing tests 2021-08-17 01:40:06 -05:00
039f92264b Dropping python 3.4 2021-08-17 01:37:18 -05:00
c2c47610c8 Fix steps in test 2021-08-17 01:32:40 -05:00
66fc79695e Attempt to fix issues with matrix and versions 2021-08-17 01:29:37 -05:00
454ebeb072 Remove extra architecture stuff 2021-08-17 01:21:12 -05:00
7ca68e5473 First attempt at github actions setup 2021-08-17 01:18:26 -05:00
484913ba0f Update version for 1.8.0 release 2021-08-08 21:51:06 -05:00
d249f0106b Fix #59, initialize and uninitialize COM for threading 2021-08-07 22:16:33 -05:00
94e1ec007a Add ability to handle pathlib paths
- Handle pathlib paths across all implementations, plat_other already did
- Move preprocessing code to common location
2021-08-07 21:48:10 -05:00
84c220cbd9 Change extra requires to filter on platform
Also created one extra `nativeLib` to replace the orignal two.  Will remove
the others after a couple releases.
2021-08-07 21:04:40 -05:00
6612545110 Add note about pyobjc to README, add extra option 2021-06-22 21:36:14 -05:00
d52b4f206c Fix CHANGES.rst issue 2021-06-21 22:22:48 -05:00
33171dde82 Update version for 1.7.1 release 2021-06-21 22:13:46 -05:00
077598d2ce Merge pull request #57 from BoboTiG/fix-windows-unc-names-legacy
Windows legacy: fix handling of UNC names
2021-06-21 22:06:06 -05:00
Mickaël Schoentgen
436686bf0f Windows legacy: fix handling of UNC names
The legacy implementation was not handling UNC names properly:

  Traceback (most recent call last):
    File "check.py", line 6, in <module>
      send2trash(str(file))
    File "\...\plat_win_legacy.py", line 79, in send2trash
      paths = [get_short_path_name(path) for path in paths]
    File "\...\plat_win_legacy.py", line 79, in <listcomp>
      paths = [get_short_path_name(path) for path in paths]
    File "\...\plat_win_legacy.py", line 62, in get_short_path_name
      raise WindowsError(err_no, FormatError(err_no), long_name[4:])
  OSError: [Errno 123] La syntaxe du nom de fichier, de répertoire ou de volume est incorrecte.: '\\\\SERVER\\folder\\file.txt'
2021-05-26 17:22:26 +02:00
21 changed files with 365 additions and 195 deletions

71
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@@ -0,0 +1,71 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
- cron: '25 5 * * 4'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'python' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

80
.github/workflows/default.yml vendored Normal file
View File

@@ -0,0 +1,80 @@
# Workflow lints, and checks format in parallel then runs tests on all platforms
name: Default CI/CD
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.x
uses: actions/setup-python@v2
with:
python-version: 3.x
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8
- name: Lint with flake8
run: |
flake8 .
test:
needs: lint
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- os: ubuntu-latest
python-version: 3.10.0-alpha - 3.10.0
- os: ubuntu-latest
python-version: 3.9
- os: ubuntu-latest
python-version: 3.8
- os: ubuntu-latest
python-version: 3.7
- os: ubuntu-latest
python-version: 3.6
- os: ubuntu-latest
python-version: 3.5
- os: ubuntu-latest
python-version: 2.7
# - os: macos-latest
# python-version: 3.9
# - os: macos-latest
# python-version: 3.8
# - os: macos-latest
# python-version: 2.7
- os: windows-latest
python-version: 3.9
- os: windows-latest
python-version: 3.8
- os: windows-latest
python-version: 2.7
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest
- name: Install windows dependencies
if: ${{ matrix.os == 'windows-latest' }}
run: |
pip install pywin32
- name: Install macOS dependencies
if: ${{ matrix.os == 'macos-latest' }}
run: |
pip install pyobjc-framework-Cocoa
- name: Run tests
run: |
pytest

View File

@@ -1,30 +1,16 @@
language: python language: python
matrix: matrix:
include: include:
- os: windows
language: sh
python: "3.8"
env: "PATH=/c/Python38:/c/Python38/Scripts:$PATH"
# Perform the manual steps on windows to install python3
before_install:
- choco install python --version=3.8.6
- python -m pip install --upgrade pip
before_script:
- export TOXENV=py38
- python: "2.7"
- python: "3.4"
- python: "3.5"
- python: "3.6"
- python: "3.7"
- python: "3.8"
- python: "3.9"
- python: "nightly" # 3.10
before_script:
- export TOXENV=py310
- python: "2.7" - python: "2.7"
arch: ppc64le arch: ppc64le
- python: "3.6" - python: "3.6"
arch: ppc64le arch: ppc64le
- python: "3.7"
arch: ppc64le
- python: "3.8"
arch: ppc64le
- python: "3.9"
arch: ppc64le
install: install:
- python -m pip install tox - python -m pip install tox
before_script: before_script:

View File

@@ -1,8 +1,31 @@
Changes Changes
======= =======
Version 1.7.0a -- 2020/04/20 Version 1.8.1b0 -- 2021/09/20
---------------------------- -----------------------------
* Add fallback to HOMETRASH when cross device errors happen in plat_other (#26, $41, #63)
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) * Add console_script entry point (#50)
* Increased python CI versions (#52, #54) * Increased python CI versions (#52, #54)
* Fix minor issue in setup.py (#53) * Fix minor issue in setup.py (#53)

View File

@@ -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
pyproject.toml Normal file
View File

@@ -0,0 +1,6 @@
[build-system]
requires = ["setuptools >= 40.6.0", "wheel"]
build-backend = "setuptools.build_meta"
[tool.black]
line-length = 120

View File

@@ -35,9 +35,7 @@ class FileOperationProgressSink(DesignatedWrapPolicy):
# Can detect cases where to stop via flags and condition below, however the operation # Can detect cases where to stop via flags and condition below, however the operation
# does not actual stop, we can resort to raising an exception as that does stop things # does not actual stop, we can resort to raising an exception as that does stop things
# but that may need some additional considerations before implementing. # but that may need some additional considerations before implementing.
return ( return 0 if flags & shellcon.TSF_DELETE_RECYCLE_IF_POSSIBLE else 0x80004005 # S_OK, or E_FAIL
0 if flags & shellcon.TSF_DELETE_RECYCLE_IF_POSSIBLE else 0x80004005
) # S_OK, or E_FAIL
def PostDeleteItem(self, flags, item, hrDelete, newlyCreated): def PostDeleteItem(self, flags, item, hrDelete, newlyCreated):
if newlyCreated: if newlyCreated:
@@ -58,31 +56,20 @@ class FileOperationProgressSink(DesignatedWrapPolicy):
def PreMoveItem(self, Flags, Item, DestinationFolder, NewName): def PreMoveItem(self, Flags, Item, DestinationFolder, NewName):
pass pass
def PostMoveItem( def PostMoveItem(self, Flags, Item, DestinationFolder, NewName, hrMove, NewlyCreated):
self, Flags, Item, DestinationFolder, NewName, hrMove, NewlyCreated
):
pass pass
def PreCopyItem(self, Flags, Item, DestinationFolder, NewName): def PreCopyItem(self, Flags, Item, DestinationFolder, NewName):
pass pass
def PostCopyItem( def PostCopyItem(self, Flags, Item, DestinationFolder, NewName, hrCopy, NewlyCreated):
self, Flags, Item, DestinationFolder, NewName, hrCopy, NewlyCreated
):
pass pass
def PreNewItem(self, Flags, DestinationFolder, NewName): def PreNewItem(self, Flags, DestinationFolder, NewName):
pass pass
def PostNewItem( def PostNewItem(
self, self, Flags, DestinationFolder, NewName, TemplateName, FileAttributes, hrNew, NewItem,
Flags,
DestinationFolder,
NewName,
TemplateName,
FileAttributes,
hrNew,
NewItem,
): ):
pass pass
@@ -100,6 +87,4 @@ class FileOperationProgressSink(DesignatedWrapPolicy):
def CreateSink(): def CreateSink():
return pythoncom.WrapObject( return pythoncom.WrapObject(FileOperationProgressSink(), shell.IID_IFileOperationProgressSink)
FileOperationProgressSink(), shell.IID_IFileOperationProgressSink
)

View File

@@ -14,20 +14,20 @@ from send2trash import send2trash
def main(args=None): def main(args=None):
parser = ArgumentParser(description='Tool to send files to trash') parser = ArgumentParser(description="Tool to send files to trash")
parser.add_argument('files', nargs='+') parser.add_argument("files", nargs="+")
parser.add_argument('-v', '--verbose', action='store_true', help='Print deleted files') parser.add_argument("-v", "--verbose", action="store_true", help="Print deleted files")
args = parser.parse_args(args) args = parser.parse_args(args)
for filename in args.files: for filename in args.files:
try: try:
send2trash(filename) send2trash(filename)
if args.verbose: if args.verbose:
print('Trashed «' + filename + '»') print("Trashed «" + filename + "»")
except OSError as e: except OSError as e:
print(str(e), file=sys.stderr) print(str(e), file=sys.stderr)
sys.exit(1) sys.exit(1)
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@@ -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)

View File

@@ -10,6 +10,7 @@ from ctypes import cdll, byref, Structure, c_char, c_char_p
from ctypes.util import find_library from ctypes.util import find_library
from .compat import binary_type from .compat import binary_type
from .util import preprocess_paths
Foundation = cdll.LoadLibrary(find_library("Foundation")) Foundation = cdll.LoadLibrary(find_library("Foundation"))
CoreServices = cdll.LoadLibrary(find_library("CoreServices")) CoreServices = cdll.LoadLibrary(find_library("CoreServices"))
@@ -40,12 +41,8 @@ def check_op_result(op_result):
def send2trash(paths): def send2trash(paths):
if not isinstance(paths, list): paths = preprocess_paths(paths)
paths = [paths] paths = [path.encode("utf-8") if not isinstance(path, binary_type) else path for path in paths]
paths = [
path.encode("utf-8") if not isinstance(path, binary_type) else path
for path in paths
]
for path in paths: for path in paths:
fp = FSRef() fp = FSRef()
opts = kFSPathMakeRefDoNotFollowLeafSymlink opts = kFSPathMakeRefDoNotFollowLeafSymlink

View File

@@ -6,6 +6,7 @@
from Foundation import NSFileManager, NSURL from Foundation import NSFileManager, NSURL
from .compat import text_type from .compat import text_type
from .util import preprocess_paths
def check_op_result(op_result): def check_op_result(op_result):
@@ -16,12 +17,8 @@ def check_op_result(op_result):
def send2trash(paths): def send2trash(paths):
if not isinstance(paths, list): paths = preprocess_paths(paths)
paths = [paths] paths = [path.decode("utf-8") if not isinstance(path, text_type) else path for path in paths]
paths = [
path.decode("utf-8") if not isinstance(path, text_type) else path
for path in paths
]
for path in paths: for path in paths:
file_url = NSURL.fileURLWithPath_(path) file_url = NSURL.fileURLWithPath_(path)
fm = NSFileManager.defaultManager() fm = NSFileManager.defaultManager()

View File

@@ -19,6 +19,7 @@ from __future__ import unicode_literals
import errno import errno
import sys import sys
import os import os
import shutil
import os.path as op import os.path as op
from datetime import datetime from datetime import datetime
import stat import stat
@@ -30,6 +31,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:
@@ -94,7 +96,7 @@ def check_create(dir):
os.makedirs(dir, 0o700) os.makedirs(dir, 0o700)
def trash_move(src, dst, topdir=None): def trash_move(src, dst, topdir=None, cross_dev=False):
filename = op.basename(src) filename = op.basename(src)
filespath = op.join(dst, FILES_DIR) filespath = op.join(dst, FILES_DIR)
infopath = op.join(dst, INFO_DIR) infopath = op.join(dst, INFO_DIR)
@@ -102,9 +104,7 @@ def trash_move(src, dst, topdir=None):
counter = 0 counter = 0
destname = filename destname = filename
while op.exists(op.join(filespath, destname)) or op.exists( while op.exists(op.join(filespath, destname)) or op.exists(op.join(infopath, destname + INFO_SUFFIX)):
op.join(infopath, destname + INFO_SUFFIX)
):
counter += 1 counter += 1
destname = base_name + b" " + text_type(counter).encode("ascii") + ext destname = base_name + b" " + text_type(counter).encode("ascii") + ext
@@ -113,14 +113,18 @@ def trash_move(src, dst, topdir=None):
with open(op.join(infopath, destname + INFO_SUFFIX), "w") as f: with open(op.join(infopath, destname + INFO_SUFFIX), "w") as f:
f.write(info_for(src, topdir)) f.write(info_for(src, topdir))
os.rename(src, op.join(filespath, destname)) destpath = op.join(filespath, destname)
if cross_dev:
shutil.move(src, destpath)
else:
os.rename(src, destpath)
def find_mount_point(path): def find_mount_point(path):
# Even if something's wrong, "/" is a mount point, so the loop will exit. # Even if something's wrong, "/" is a mount point, so the loop will exit.
# Use realpath in case it's a symlink # Use realpath in case it's a symlink
path = op.realpath(path) # Required to avoid infinite loop path = op.realpath(path) # Required to avoid infinite loop
while not op.ismount(path): while not op.ismount(path): # Note ismount() does not always detect mounts
path = op.split(path)[0] path = op.split(path)[0]
return path return path
@@ -172,8 +176,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)
@@ -208,4 +211,11 @@ def send2trash(paths):
if trash_dev != path_dev: if trash_dev != path_dev:
raise OSError("Couldn't find mount point for %s" % path) raise OSError("Couldn't find mount point for %s" % path)
dest_trash = find_ext_volume_trash(topdir) dest_trash = find_ext_volume_trash(topdir)
trash_move(path_b, dest_trash, topdir) try:
trash_move(path_b, dest_trash, topdir)
except OSError as error:
# Cross link errors default back to HOMETRASH
if error.errno == errno.EXDEV:
trash_move(path_b, HOMETRASH_B, XDG_DATA_HOME, cross_dev=True)
else:
raise

View File

@@ -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,28 +53,59 @@ 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 for path in paths]
text_type(path, "mbcs") if not isinstance(path, text_type) else path
for path in paths
]
# convert to full paths # convert to full 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

View File

@@ -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,35 +16,25 @@ 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 for path in paths]
text_type(path, "mbcs") if not isinstance(path, text_type) else path
for path in paths
]
# convert to full paths # convert to full 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,
) )
# default flags to use # default flags to use
flags = ( flags = shellcon.FOF_NOCONFIRMATION | shellcon.FOF_NOERRORUI | shellcon.FOF_SILENT | shellcon.FOFX_EARLYFAILURE
shellcon.FOF_NOCONFIRMATION
| shellcon.FOF_NOERRORUI
| shellcon.FOF_SILENT
| shellcon.FOFX_EARLYFAILURE
)
# determine rest of the flags based on OS version # determine rest of the flags based on OS version
# use newer recommended flags if available # use newer recommended flags if available
if int(version().split(".", 1)[0]) >= 8: if int(version().split(".", 1)[0]) >= 8:
flags |= ( flags |= 0x20000000 | 0x00080000 # FOFX_ADDUNDORECORD win 8+ # FOFX_RECYCLEONDELETE win 8+
0x20000000 # FOFX_ADDUNDORECORD win 8+
| 0x00080000 # FOFX_RECYCLEONDELETE win 8+
)
else: else:
flags |= shellcon.FOF_ALLOWUNDO flags |= shellcon.FOF_ALLOWUNDO
# set the flags # set the flags
@@ -65,3 +56,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()

14
send2trash/util.py Normal file
View File

@@ -0,0 +1,14 @@
# 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

45
setup.cfg Normal file
View File

@@ -0,0 +1,45 @@
[metadata]
name = Send2Trash
version = 1.8.1b0
url = https://github.com/arsenetar/send2trash
project_urls =
Bug Reports = https://github.com/arsenetar/send2trash/issues
author = Andrew Senetar
author_email = arsenetar@voltaicideas.net
license = BSD License
license_files = LICENSE
description = Send file to trash natively under Mac OS X, Windows and Linux
long_description = file:README.rst
long_description_content_type = text/x-rst
classifiers =
Development Status :: 5 - Production/Stable
Intended Audience :: Developers
License :: OSI Approved :: BSD License
Operating System :: MacOS :: MacOS X
Operating System :: Microsoft :: Windows
Operating System :: POSIX
Programming Language :: Python :: 2.7
Programming Language :: Python :: 3
Programming Language :: Python :: 3.5
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Topic :: Desktop Environment :: File Managers
[options]
packages = send2trash
tests_require = pytest
python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*
[options.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"
[options.entry_points]
console_scripts =
send2trash = send2trash.__main__:main

View File

@@ -1,41 +0,0 @@
from setuptools import setup
CLASSIFIERS = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: BSD License",
"Operating System :: MacOS :: MacOS X",
"Operating System :: Microsoft :: Windows",
"Operating System :: POSIX",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Topic :: Desktop Environment :: File Managers",
]
with open("README.rst", "rt") as f1, open("CHANGES.rst", "rt") as f2:
LONG_DESCRIPTION = f1.read() + "\n\n" + f2.read()
setup(
name="Send2Trash",
version="1.7.0a1",
author="Andrew Senetar",
author_email="arsenetar@voltaicideas.net",
packages=["send2trash"],
scripts=[],
test_suite="tests",
url="https://github.com/arsenetar/send2trash",
license="BSD License",
description="Send file to trash natively under Mac OS X, Windows and Linux.",
long_description=LONG_DESCRIPTION,
classifiers=CLASSIFIERS,
extras_require={"win32": ["pywin32"]},
project_urls={"Bug Reports": "https://github.com/arsenetar/send2trash/issues"},
entry_points={"console_scripts": ["send2trash=send2trash.__main__:main"]},
)

View File

@@ -28,9 +28,7 @@ else:
@pytest.fixture @pytest.fixture
def testfile(): def testfile():
file = NamedTemporaryFile( file = NamedTemporaryFile(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 assert op.exists(file.name) is True
yield file yield file
@@ -50,9 +48,7 @@ def testfiles():
files = list( files = list(
map( map(
lambda index: NamedTemporaryFile( lambda index: NamedTemporaryFile(
dir=op.expanduser("~"), dir=op.expanduser("~"), prefix="send2trash_test{}".format(index), delete=False,
prefix="send2trash_test{}".format(index),
delete=False,
), ),
range(10), range(10),
) )
@@ -62,10 +58,7 @@ def testfiles():
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]
[ [os.remove(op.join(HOMETRASH, "info", filename + ".trashinfo")) for filename in filenames]
os.remove(op.join(HOMETRASH, "info", filename + ".trashinfo"))
for filename in filenames
]
def test_trash(testfile): def test_trash(testfile):
@@ -136,10 +129,7 @@ class ExtVol:
return st.st_dev return st.st_dev
def s_ismount(path): def s_ismount(path):
if op.realpath(path) in ( if op.realpath(path) in (op.realpath(self.trashTopdir), op.realpath(self.trashTopdir_b),):
op.realpath(self.trashTopdir),
op.realpath(self.trashTopdir_b),
):
return True return True
return old_ismount(path) return old_ismount(path)
@@ -172,15 +162,8 @@ def test_trash_topdir(testExtVol):
s2t(testExtVol[2]) s2t(testExtVol[2])
assert op.exists(testExtVol[2]) is False assert op.exists(testExtVol[2]) is False
assert ( assert op.exists(op.join(trashDir, str(os.getuid()), "files", testExtVol[1])) is True
op.exists(op.join(trashDir, str(os.getuid()), "files", testExtVol[1])) is True assert op.exists(op.join(trashDir, str(os.getuid()), "info", testExtVol[1] + ".trashinfo",)) is True
)
assert (
op.exists(
op.join(trashDir, str(os.getuid()), "info", testExtVol[1] + ".trashinfo",)
)
is True
)
# info relative path (if another test is added, with the same fileName/Path, # info relative path (if another test is added, with the same fileName/Path,
# then it gets renamed etc.) # then it gets renamed etc.)
cfg = ConfigParser() cfg = ConfigParser()
@@ -191,17 +174,7 @@ def test_trash_topdir(testExtVol):
def test_trash_topdir_fallback(testExtVol): def test_trash_topdir_fallback(testExtVol):
s2t(testExtVol[2]) s2t(testExtVol[2])
assert op.exists(testExtVol[2]) is False assert op.exists(testExtVol[2]) is False
assert ( assert op.exists(op.join(testExtVol[0].trashTopdir, ".Trash-" + str(os.getuid()), "files", testExtVol[1],)) is True
op.exists(
op.join(
testExtVol[0].trashTopdir,
".Trash-" + str(os.getuid()),
"files",
testExtVol[1],
)
)
is True
)
def test_trash_topdir_failure(testExtVol): def test_trash_topdir_failure(testExtVol):
@@ -221,15 +194,5 @@ def test_trash_symlink(testExtVol):
os.symlink(op.join(testExtVol[0].trashTopdir, "subdir"), slDir) os.symlink(op.join(testExtVol[0].trashTopdir, "subdir"), slDir)
s2t(op.join(slDir, testExtVol[1])) s2t(op.join(slDir, testExtVol[1]))
assert op.exists(filePath) is False assert op.exists(filePath) is False
assert ( assert op.exists(op.join(testExtVol[0].trashTopdir, ".Trash-" + str(os.getuid()), "files", testExtVol[1],)) is True
op.exists(
op.join(
testExtVol[0].trashTopdir,
".Trash-" + str(os.getuid()),
"files",
testExtVol[1],
)
)
is True
)
os.remove(slDir) os.remove(slDir)

View File

@@ -144,7 +144,10 @@ def longdir(tmp_path):
dirname = "\\\\?\\" + str(tmp_path) dirname = "\\\\?\\" + str(tmp_path)
name = "A" * 100 name = "A" * 100
yield op.join(dirname, name, name, name) yield op.join(dirname, name, name, name)
shutil.rmtree(dirname, ignore_errors=True) try:
shutil.rmtree(dirname, ignore_errors=True)
except TypeError:
pass
@pytest.fixture @pytest.fixture

View File

@@ -14,9 +14,7 @@ if sys.platform != "win32":
@pytest.fixture @pytest.fixture
def file(): def file():
file = NamedTemporaryFile( file = NamedTemporaryFile(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 # Verify file was actually created
assert op.exists(file.name) is True assert op.exists(file.name) is True

View File

@@ -1,6 +1,7 @@
[tox] [tox]
envlist = py{27,34,35,36,37,38,39,310} envlist = py{27,34,35,36,37,38,39,310}
skip_missing_interpreters = True skip_missing_interpreters = True
isolated_build = True
[testenv] [testenv]
deps = deps =
@@ -20,4 +21,5 @@ deps =
[flake8] [flake8]
exclude = .tox,env,build exclude = .tox,env,build
max-line-length = 120 max-line-length = 120
ignore = E731,E203,E501,W503 select = C,E,F,W,B,B950
extend-ignore = E203, E501