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

39 Commits

Author SHA1 Message Date
0a48c26f68 Merge pull request #88 from PalmtopTiger/iterable-types
Support for any iterable type as input data
2024-02-19 09:08:53 -06:00
Andrey Efremov
ed039dc892 Support for any iterable type as input data 2024-01-11 14:25:22 +07:00
e59ddcae98 Merge pull request #79 from mgorny/wheel
Remove redundant wheel dep from pyproject.toml
2023-05-03 02:41:22 -05:00
Michał Górny
19cf5d941a Remove redundant wheel dep from pyproject.toml
Remove the redundant `wheel` dependency, as it is added by the backend
automatically.  Listing it explicitly in the documentation was
a historical mistake and has been fixed since, see:
f7d30a9529
2023-04-27 18:12:32 +02:00
0244f53e2e fix(build): Fix syntax in setup.cfg, add python 3.11 2023-04-27 00:28:59 -05:00
1625d56345 Update version & changelog for 1.8.2 2023-04-27 00:22:55 -05:00
63e770d29e Merge pull request #78 from arsenetar/as/fix-ci
fix(ci): Update to latest actions, fix python versions
2023-04-27 00:03:55 -05:00
bfd3e08661 fix(ci): Update to latest actions, fix python versions
- Update to latest actions
- Change OS for older python 3.6, 3.5 to run
2023-04-26 23:54:52 -05:00
490fe02245 Merge pull request #73 from sobolevn/patch-1
TravisCI is not used anymore
2023-04-26 23:54:24 -05:00
d6d904c774 Merge pull request #77 from BoboTiG/patch-1
win/legacy: tiny logic simplification
2023-04-26 23:36:39 -05:00
Mickaël Schoentgen
0a36688a4b win/legacy: tiny logic simplification 2023-04-02 17:08:25 +02:00
Nikita Sobolev
955f8091da TravisCI is not used anymore 2022-10-11 23:57:46 +03:00
0ef9b3294a fix(tests): Correct windows tests to run on python <3.6 2022-07-26 22:51:16 -05:00
4b9bc4bc31 fix(win): Prevent exception on empty list
Add check for when an empty list remains after preprocessing and do
not continue for both legacy and modern windows implementations.

Fix #71
2022-07-26 22:42:30 -05:00
be402728fb Update setup.cfg for changes in d37197c, fix #68. 2022-06-01 02:06:14 -05:00
448224954b Replace relative imports with absolute 2022-06-01 00:22:46 -05:00
d37197c4f7 Move mac/win to subpackages & fix #64
- Move macOS and Windows implementations to sub packagese to improve organization
- Fix #64 in legacy windows implementation by mapping results to standard error codes
2022-04-30 19:52:09 -05:00
2a88b82104 Fix test_plat_other from previous change 2021-08-24 01:21:12 -05:00
18e51c0b5a Minor cleanup in plat_other
- Add OSError code values
- Use INFO_SUFFIX constant in tests
- Remove old PathLike conversions
2021-08-24 01:00:02 -05:00
7686647389 Fix flake8 error 2021-08-21 16:04:05 -05:00
696aed558b Change method for test symlink path generation 2021-08-21 16:00:50 -05:00
007d84361a Fix items missed in test_plat_other in last commit 2021-08-21 15:22:59 -05:00
78a536abba Minor code quality updates 2021-08-21 15:19:32 -05:00
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
26 changed files with 474 additions and 343 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@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
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@v2
# 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@v2

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

@@ -0,0 +1,82 @@
# 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@v3
- name: Set up Python 3.x
uses: actions/setup-python@v3
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.11
- os: ubuntu-latest
python-version: "3.10"
- os: ubuntu-latest
python-version: 3.9
- os: ubuntu-latest
python-version: 3.8
- os: ubuntu-latest
python-version: 3.7
- os: ubuntu-20.04
python-version: 3.6
- os: ubuntu-20.04
python-version: 3.5
- os: ubuntu-latest
python-version: 2.7
# - os: macos-latest
# python-version: 3.11
# - os: macos-latest
# python-version: 3.8
# - os: macos-latest
# python-version: 2.7
- os: windows-latest
python-version: 3.11
- os: windows-latest
python-version: 3.8
- os: windows-latest
python-version: 2.7
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
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,33 +0,0 @@
language: python
matrix:
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"
arch: ppc64le
- python: "3.6"
arch: ppc64le
install:
- python -m pip install tox
before_script:
- export TOXENV=$(echo py$TRAVIS_PYTHON_VERSION | tr -d .)
script:
- python -m tox

View File

@@ -1,6 +1,16 @@
Changes Changes
======= =======
Version 1.8.2 -- 2023/04/27
---------------------------
* win/legacy: tiny logic simplification by @BoboTiG in https://github.com/arsenetar/send2trash/pull/77
* TravisCI is not used anymore by @sobolevn in https://github.com/arsenetar/send2trash/pull/73
* fix(ci): Update to latest actions, fix python versions by @arsenetar in https://github.com/arsenetar/send2trash/pull/78
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 Version 1.8.0 -- 2021/08/08
--------------------------- ---------------------------

6
pyproject.toml Normal file
View File

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

View File

@@ -1,105 +0,0 @@
# Sample implementation of IFileOperationProgressSink that just prints
# some basic info
import pythoncom
from win32com.shell import shell, shellcon
from win32com.server.policy import DesignatedWrapPolicy
class FileOperationProgressSink(DesignatedWrapPolicy):
_com_interfaces_ = [shell.IID_IFileOperationProgressSink]
_public_methods_ = [
"StartOperations",
"FinishOperations",
"PreRenameItem",
"PostRenameItem",
"PreMoveItem",
"PostMoveItem",
"PreCopyItem",
"PostCopyItem",
"PreDeleteItem",
"PostDeleteItem",
"PreNewItem",
"PostNewItem",
"UpdateProgress",
"ResetTimer",
"PauseTimer",
"ResumeTimer",
]
def __init__(self):
self._wrap_(self)
self.newItem = None
def PreDeleteItem(self, flags, item):
# 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
# but that may need some additional considerations before implementing.
return (
0 if flags & shellcon.TSF_DELETE_RECYCLE_IF_POSSIBLE else 0x80004005
) # S_OK, or E_FAIL
def PostDeleteItem(self, flags, item, hrDelete, newlyCreated):
if newlyCreated:
self.newItem = newlyCreated.GetDisplayName(shellcon.SHGDN_FORPARSING)
def StartOperations(self):
pass
def FinishOperations(self, Result):
pass
def PreRenameItem(self, Flags, Item, NewName):
pass
def PostRenameItem(self, Flags, Item, NewName, hrRename, NewlyCreated):
pass
def PreMoveItem(self, Flags, Item, DestinationFolder, NewName):
pass
def PostMoveItem(
self, Flags, Item, DestinationFolder, NewName, hrMove, NewlyCreated
):
pass
def PreCopyItem(self, Flags, Item, DestinationFolder, NewName):
pass
def PostCopyItem(
self, Flags, Item, DestinationFolder, NewName, hrCopy, NewlyCreated
):
pass
def PreNewItem(self, Flags, DestinationFolder, NewName):
pass
def PostNewItem(
self,
Flags,
DestinationFolder,
NewName,
TemplateName,
FileAttributes,
hrNew,
NewItem,
):
pass
def UpdateProgress(self, WorkTotal, WorkSoFar):
pass
def ResetTimer(self):
pass
def PauseTimer(self):
pass
def ResumeTimer(self):
pass
def CreateSink():
return pythoncom.WrapObject(
FileOperationProgressSink(), shell.IID_IFileOperationProgressSink
)

View File

@@ -6,16 +6,16 @@
import sys import sys
from .exceptions import TrashPermissionError # noqa: F401 from send2trash.exceptions import TrashPermissionError # noqa: F401
if sys.platform == "darwin": if sys.platform == "darwin":
from .plat_osx import send2trash from send2trash.mac import send2trash
elif sys.platform == "win32": elif sys.platform == "win32":
from .plat_win import send2trash from send2trash.win import send2trash
else: else:
try: try:
# If we can use gio, let's use it # If we can use gio, let's use it
from .plat_gio import send2trash from send2trash.plat_gio import send2trash
except ImportError: except ImportError:
# Oh well, let's fallback to our own Freedesktop trash implementation # Oh well, let's fallback to our own Freedesktop trash implementation
from .plat_other import send2trash # noqa: F401 from send2trash.plat_other import send2trash # noqa: F401

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

@@ -18,3 +18,8 @@ else:
text_type = unicode # noqa: F821 text_type = unicode # noqa: F821
binary_type = str binary_type = str
environb = os.environ environb = os.environ
try:
from collections.abc import Iterable as iterable_type
except ImportError:
from collections import Iterable as iterable_type

View File

@@ -1,5 +1,5 @@
import errno import errno
from .compat import PY3 from send2trash.compat import PY3
if PY3: if PY3:
_permission_error = PermissionError # noqa: F821 _permission_error = PermissionError # noqa: F821

View File

@@ -11,10 +11,10 @@ from sys import version_info
macos_ver = tuple(int(part) for part in mac_ver()[0].split(".")) macos_ver = tuple(int(part) for part in mac_ver()[0].split("."))
if version_info >= (3, 6) and macos_ver >= (10, 9): if version_info >= (3, 6) and macos_ver >= (10, 9):
try: try:
from .plat_osx_pyobjc import send2trash from send2trash.mac.modern import send2trash
except ImportError: except ImportError:
# Try to fall back to ctypes version, although likely problematic still # Try to fall back to ctypes version, although likely problematic still
from .plat_osx_ctypes import send2trash from send2trash.mac.legacy import send2trash
else: else:
# Just use the old version otherwise # Just use the old version otherwise
from .plat_osx_ctypes import send2trash # noqa: F401 from send2trash.mac.legacy import send2trash # noqa: F401

View File

@@ -9,8 +9,8 @@ from __future__ import unicode_literals
from ctypes import cdll, byref, Structure, c_char, c_char_p 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 send2trash.compat import binary_type
from .util import preprocess_paths from send2trash.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"))
@@ -42,10 +42,7 @@ def check_op_result(op_result):
def send2trash(paths): def send2trash(paths):
paths = preprocess_paths(paths) paths = preprocess_paths(paths)
paths = [ paths = [path.encode("utf-8") if not isinstance(path, binary_type) else path for path in 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

@@ -5,8 +5,8 @@
# http://www.hardcoded.net/licenses/bsd_license # http://www.hardcoded.net/licenses/bsd_license
from Foundation import NSFileManager, NSURL from Foundation import NSFileManager, NSURL
from .compat import text_type from send2trash.compat import text_type
from .util import preprocess_paths from send2trash.util import preprocess_paths
def check_op_result(op_result): def check_op_result(op_result):
@@ -18,10 +18,7 @@ def check_op_result(op_result):
def send2trash(paths): def send2trash(paths):
paths = preprocess_paths(paths) paths = preprocess_paths(paths)
paths = [ paths = [path.decode("utf-8") if not isinstance(path, text_type) else path for path in 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

@@ -5,8 +5,8 @@
# http://www.hardcoded.net/licenses/bsd_license # http://www.hardcoded.net/licenses/bsd_license
from gi.repository import GObject, Gio from gi.repository import GObject, Gio
from .exceptions import TrashPermissionError from send2trash.exceptions import TrashPermissionError
from .util import preprocess_paths from send2trash.util import preprocess_paths
def send2trash(paths): def send2trash(paths):

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
@@ -29,9 +30,9 @@ except ImportError:
# Python 2 # Python 2
from urllib import quote from urllib import quote
from .compat import text_type, environb from send2trash.compat import text_type, environb
from .util import preprocess_paths from send2trash.util import preprocess_paths
from .exceptions import TrashPermissionError from send2trash.exceptions import TrashPermissionError
try: try:
fsencode = os.fsencode # Python 3 fsencode = os.fsencode # Python 3
@@ -95,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)
@@ -103,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
@@ -114,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
@@ -179,26 +182,23 @@ def send2trash(paths):
path_b = fsencode(path) path_b = fsencode(path)
elif isinstance(path, bytes): elif isinstance(path, bytes):
path_b = path path_b = path
elif hasattr(path, "__fspath__"):
# Python 3.6 PathLike protocol
return send2trash(path.__fspath__())
else: else:
raise TypeError("str, bytes or PathLike expected, not %r" % type(path)) raise TypeError("str, bytes or PathLike expected, not %r" % type(path))
if not op.exists(path_b): if not op.exists(path_b):
raise OSError("File not found: %s" % path) raise OSError(errno.ENOENT, "File not found: %s" % path)
# ...should check whether the user has the necessary permissions to delete # ...should check whether the user has the necessary permissions to delete
# it, before starting the trashing operation itself. [2] # it, before starting the trashing operation itself. [2]
if not os.access(path_b, os.W_OK): if not os.access(path_b, os.W_OK):
raise OSError("Permission denied: %s" % path) raise OSError(errno.EACCES, "Permission denied: %s" % path)
# if the file to be trashed is on the same device as HOMETRASH we
# want to move it there.
path_dev = get_dev(path_b)
path_dev = get_dev(path_b)
# If XDG_DATA_HOME or HOMETRASH do not yet exist we need to stat the # If XDG_DATA_HOME or HOMETRASH do not yet exist we need to stat the
# home directory, and these paths will be created further on if needed. # home directory, and these paths will be created further on if needed.
trash_dev = get_dev(op.expanduser(b"~")) trash_dev = get_dev(op.expanduser(b"~"))
# if the file to be trashed is on the same device as HOMETRASH we
# want to move it there.
if path_dev == trash_dev: if path_dev == trash_dev:
topdir = XDG_DATA_HOME topdir = XDG_DATA_HOME
dest_trash = HOMETRASH_B dest_trash = HOMETRASH_B
@@ -208,4 +208,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)
try:
trash_move(path_b, dest_trash, topdir) 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

@@ -5,12 +5,14 @@
# 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 send2trash.compat import text_type, binary_type, iterable_type
def preprocess_paths(paths): def preprocess_paths(paths):
if not isinstance(paths, list): if isinstance(paths, iterable_type) and not isinstance(paths, (text_type, binary_type)):
paths = list(paths)
elif not isinstance(paths, list):
paths = [paths] paths = [paths]
# Convert items such as pathlib paths to strings # Convert items such as pathlib paths to strings
paths = [ paths = [path.__fspath__() if hasattr(path, "__fspath__") else path for path in paths]
path.__fspath__() if hasattr(path, "__fspath__") else path for path in paths
]
return paths return paths

View File

@@ -0,0 +1,46 @@
# Sample implementation of IFileOperationProgressSink that just prints
# some basic info
import pythoncom
from win32com.shell import shell, shellcon
from win32com.server.policy import DesignatedWrapPolicy
class FileOperationProgressSink(DesignatedWrapPolicy):
_com_interfaces_ = [shell.IID_IFileOperationProgressSink]
_public_methods_ = [
"StartOperations",
"FinishOperations",
"PreRenameItem",
"PostRenameItem",
"PreMoveItem",
"PostMoveItem",
"PreCopyItem",
"PostCopyItem",
"PreDeleteItem",
"PostDeleteItem",
"PreNewItem",
"PostNewItem",
"UpdateProgress",
"ResetTimer",
"PauseTimer",
"ResumeTimer",
]
def __init__(self):
self._wrap_(self)
self.newItem = None
def PreDeleteItem(self, flags, item):
# 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
# but that may need some additional considerations before implementing.
return 0 if flags & shellcon.TSF_DELETE_RECYCLE_IF_POSSIBLE else 0x80004005 # S_OK, or E_FAIL
def PostDeleteItem(self, flags, item, hr_delete, newly_created):
if newly_created:
self.newItem = newly_created.GetDisplayName(shellcon.SHGDN_FORPARSING)
def create_sink():
return pythoncom.WrapObject(FileOperationProgressSink(), shell.IID_IFileOperationProgressSink)

View File

@@ -11,10 +11,10 @@ from platform import version
if int(version().split(".", 1)[0]) >= 6: if int(version().split(".", 1)[0]) >= 6:
try: try:
# Attempt to use pywin32 to use IFileOperation # Attempt to use pywin32 to use IFileOperation
from .plat_win_modern import send2trash from send2trash.win.modern import send2trash
except ImportError: except ImportError:
# use SHFileOperation as fallback # use SHFileOperation as fallback
from .plat_win_legacy import send2trash from send2trash.win.legacy import send2trash
else: else:
# use SHFileOperation as fallback # use SHFileOperation as fallback
from .plat_win_legacy import send2trash # noqa: F401 from send2trash.win.legacy import send2trash # noqa: F401

View File

@@ -6,8 +6,9 @@
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 .util import preprocess_paths from send2trash.compat import text_type
from send2trash.util import preprocess_paths
from ctypes import ( from ctypes import (
windll, windll,
@@ -53,6 +54,41 @@ FOF_ALLOWUNDO = 64
FOF_NOERRORUI = 1024 FOF_NOERRORUI = 1024
def convert_sh_file_opt_result(result):
# map overlapping values from SHFileOpterationW to approximate standard windows errors
# ref https://docs.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shfileoperationw#return-value
# ref https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499-
results = {
0x71: 0x50, # DE_SAMEFILE -> ERROR_FILE_EXISTS
0x72: 0x57, # DE_MANYSRC1DEST -> ERROR_INVALID_PARAMETER
0x73: 0x57, # DE_DIFFDIR -> ERROR_INVALID_PARAMETER
0x74: 0x57, # DE_ROOTDIR -> ERROR_INVALID_PARAMETER
0x75: 0x4C7, # DE_OPCANCELLED -> ERROR_CANCELLED
0x76: 0x57, # DE_DESTSUBTREE -> ERROR_INVALID_PARAMETER
0x78: 0x05, # DE_ACCESSDENIEDSRC -> ERROR_ACCESS_DENIED
0x79: 0x6F, # DE_PATHTOODEEP -> ERROR_BUFFER_OVERFLOW
0x7A: 0x57, # DE_MANYDEST -> ERROR_INVALID_PARAMETER
0x7C: 0xA1, # DE_INVALIDFILES -> ERROR_BAD_PATHNAME
0x7D: 0x57, # DE_DESTSAMETREE -> ERROR_INVALID_PARAMETER
0x7E: 0xB7, # DE_FLDDESTISFILE -> ERROR_ALREADY_EXISTS
0x80: 0xB7, # DE_FILEDESTISFLD -> ERROR_ALREADY_EXISTS
0x81: 0x6F, # DE_FILENAMETOOLONG -> ERROR_BUFFER_OVERFLOW
0x82: 0x13, # DE_DEST_IS_CDROM -> ERROR_WRITE_PROTECT
0x83: 0x13, # DE_DEST_IS_DVD -> ERROR_WRITE_PROTECT
0x84: 0x6F9, # DE_DEST_IS_CDRECORD -> ERROR_UNRECOGNIZED_MEDIA
0x85: 0xDF, # DE_FILE_TOO_LARGE -> ERROR_FILE_TOO_LARGE
0x86: 0x13, # DE_SRC_IS_CDROM -> ERROR_WRITE_PROTECT
0x87: 0x13, # DE_SRC_IS_DVD -> ERROR_WRITE_PROTECT
0x88: 0x6F9, # DE_SRC_IS_CDRECORD -> ERROR_UNRECOGNIZED_MEDIA
0xB7: 0x6F, # DE_ERROR_MAX -> ERROR_BUFFER_OVERFLOW
0x402: 0xA1, # UNKNOWN -> ERROR_BAD_PATHNAME
0x10000: 0x1D, # ERRORONDEST -> ERROR_WRITE_FAULT
0x10074: 0x57, # DE_ROOTDIR | ERRORONDEST -> ERROR_INVALID_PARAMETER
}
return results.get(result, result)
def prefix_and_path(path): def prefix_and_path(path):
r"""Guess the long-path prefix based on the kind of *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) Local paths (C:\folder\file.ext) and UNC names (\\server\folder\file.ext)
@@ -104,11 +140,10 @@ def get_short_path_name(long_name):
def send2trash(paths): def send2trash(paths):
paths = preprocess_paths(paths) paths = preprocess_paths(paths)
if not paths:
return
# 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
@@ -144,4 +179,5 @@ def send2trash(paths):
fileop.lpszProgressTitle = None fileop.lpszProgressTitle = None
result = SHFileOperationW(byref(fileop)) result = SHFileOperationW(byref(fileop))
if result: if result:
raise WindowsError(result, FormatError(result), paths) error = convert_sh_file_opt_result(result)
raise WindowsError(None, FormatError(error), paths, error)

View File

@@ -6,22 +6,21 @@
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 send2trash.compat import text_type
from .util import preprocess_paths from send2trash.util import preprocess_paths
from platform import version from platform import version
import pythoncom import pythoncom
import pywintypes import pywintypes
from win32com.shell import shell, shellcon from win32com.shell import shell, shellcon
from .IFileOperationProgressSink import CreateSink from send2trash.win.IFileOperationProgressSink import create_sink
def send2trash(paths): def send2trash(paths):
paths = preprocess_paths(paths) paths = preprocess_paths(paths)
if not paths:
return
# 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
@@ -30,22 +29,17 @@ def send2trash(paths):
pythoncom.CoInitialize() 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
@@ -53,7 +47,7 @@ def send2trash(paths):
# actually try to perform the operation, this section may throw a # actually try to perform the operation, this section may throw a
# pywintypes.com_error which does not seem to create as nice of an # pywintypes.com_error which does not seem to create as nice of an
# error as OSError so wrapping with try to convert # error as OSError so wrapping with try to convert
sink = CreateSink() sink = create_sink()
try: try:
for path in paths: for path in paths:
item = shell.SHCreateItemFromParsingName(path, None, shell.IID_IShellItem) item = shell.SHCreateItemFromParsingName(path, None, shell.IID_IShellItem)

52
setup.cfg Normal file
View File

@@ -0,0 +1,52 @@
[metadata]
name = Send2Trash
version = 1.8.2
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
Programming Language :: Python :: 3.11
Topic :: Desktop Environment :: File Managers
[options]
packages = find:
tests_require = pytest
python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*
[options.packages.find]
include=
send2trash*
[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,49 +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.8.0",
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,
long_description_content_type="text/x-rst",
classifiers=CLASSIFIERS,
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"},
entry_points={"console_scripts": ["send2trash=send2trash.__main__:main"]},
)

View File

@@ -13,14 +13,16 @@ except ImportError:
# py2 # py2
from ConfigParser import ConfigParser # noqa: F401 from ConfigParser import ConfigParser # noqa: F401
from tempfile import mkdtemp, NamedTemporaryFile, mktemp from tempfile import mkdtemp, NamedTemporaryFile
import shutil import shutil
import stat import stat
import uuid
if sys.platform != "win32": if sys.platform != "win32":
import send2trash.plat_other import send2trash.plat_other
from send2trash.plat_other import send2trash as s2t from send2trash.plat_other import send2trash as s2t
INFO_SUFFIX = send2trash.plat_other.INFO_SUFFIX.decode()
HOMETRASH = send2trash.plat_other.HOMETRASH HOMETRASH = send2trash.plat_other.HOMETRASH
else: else:
pytest.skip("Skipping non-windows tests", allow_module_level=True) pytest.skip("Skipping non-windows tests", allow_module_level=True)
@@ -28,9 +30,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
@@ -40,7 +40,7 @@ def testfile():
# Remove trash files if they exist # Remove trash files if they exist
if op.exists(op.join(HOMETRASH, "files", name)): if op.exists(op.join(HOMETRASH, "files", name)):
os.remove(op.join(HOMETRASH, "files", name)) os.remove(op.join(HOMETRASH, "files", name))
os.remove(op.join(HOMETRASH, "info", name + ".trashinfo")) os.remove(op.join(HOMETRASH, "info", name + INFO_SUFFIX))
if op.exists(file.name): if op.exists(file.name):
os.remove(file.name) os.remove(file.name)
@@ -62,10 +62,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 + INFO_SUFFIX)) for filename in filenames]
os.remove(op.join(HOMETRASH, "info", filename + ".trashinfo"))
for filename in filenames
]
def test_trash(testfile): def test_trash(testfile):
@@ -91,54 +88,52 @@ def _filesys_enc():
@pytest.fixture @pytest.fixture
def testUnicodefile(): def gen_unicode_file():
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 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" and op.exists(op.join(HOMETRASH, "files", name)):
# Remove trash files if they exist
if op.exists(op.join(HOMETRASH, "files", name)):
os.remove(op.join(HOMETRASH, "files", name)) os.remove(op.join(HOMETRASH, "files", name))
os.remove(op.join(HOMETRASH, "info", name + ".trashinfo")) os.remove(op.join(HOMETRASH, "info", name + INFO_SUFFIX))
if op.exists(file): if op.exists(file):
os.remove(file) os.remove(file)
@pytest.mark.skipif(_filesys_enc() == "ascii", reason="Requires Unicode filesystem") @pytest.mark.skipif(_filesys_enc() == "ascii", reason="Requires Unicode filesystem")
def test_trash_bytes(testUnicodefile): def test_trash_bytes(gen_unicode_file):
s2t(testUnicodefile) s2t(gen_unicode_file)
assert not op.exists(testUnicodefile) assert not op.exists(gen_unicode_file)
@pytest.mark.skipif(_filesys_enc() == "ascii", reason="Requires Unicode filesystem") @pytest.mark.skipif(_filesys_enc() == "ascii", reason="Requires Unicode filesystem")
def test_trash_unicode(testUnicodefile): def test_trash_unicode(gen_unicode_file):
s2t(testUnicodefile.decode(sys.getfilesystemencoding())) s2t(gen_unicode_file.decode(sys.getfilesystemencoding()))
assert not op.exists(testUnicodefile) assert not op.exists(gen_unicode_file)
class ExtVol: class ExtVol:
def __init__(self, path): def __init__(self, path):
self.trashTopdir = path self.trash_topdir = path
if PY3: if PY3:
self.trashTopdir_b = os.fsencode(self.trashTopdir) self.trash_topdir_b = os.fsencode(self.trash_topdir)
else: else:
self.trashTopdir_b = self.trashTopdir self.trash_topdir_b = self.trash_topdir
def s_getdev(path): def s_getdev(path):
from send2trash.plat_other import is_parent from send2trash.plat_other import is_parent
st = os.lstat(path) st = os.lstat(path)
if is_parent(self.trashTopdir, path): if is_parent(self.trash_topdir, path):
return "dev" return "dev"
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.trash_topdir),
op.realpath(self.trashTopdir_b), op.realpath(self.trash_topdir_b),
): ):
return True return True
return old_ismount(path) return old_ismount(path)
@@ -151,85 +146,86 @@ class ExtVol:
def cleanup(self): def cleanup(self):
send2trash.plat_other.get_dev = self.old_getdev send2trash.plat_other.get_dev = self.old_getdev
send2trash.plat_other.os.path.ismount = self.old_ismount send2trash.plat_other.os.path.ismount = self.old_ismount
shutil.rmtree(self.trashTopdir) shutil.rmtree(self.trash_topdir)
@pytest.fixture @pytest.fixture
def testExtVol(): def gen_ext_vol():
trashTopdir = mkdtemp(prefix="s2t") trash_topdir = mkdtemp(prefix="s2t")
volume = ExtVol(trashTopdir) volume = ExtVol(trash_topdir)
fileName = "test.txt" file_name = "test.txt"
filePath = op.join(volume.trashTopdir, fileName) file_path = op.join(volume.trash_topdir, file_name)
touch(filePath) touch(file_path)
assert op.exists(filePath) is True assert op.exists(file_path) is True
yield volume, fileName, filePath yield volume, file_name, file_path
volume.cleanup() volume.cleanup()
def test_trash_topdir(testExtVol): def test_trash_topdir(gen_ext_vol):
trashDir = op.join(testExtVol[0].trashTopdir, ".Trash") trash_dir = op.join(gen_ext_vol[0].trash_topdir, ".Trash")
os.mkdir(trashDir, 0o777 | stat.S_ISVTX) os.mkdir(trash_dir, 0o777 | stat.S_ISVTX)
s2t(testExtVol[2]) s2t(gen_ext_vol[2])
assert op.exists(testExtVol[2]) is False assert op.exists(gen_ext_vol[2]) is False
assert ( assert op.exists(op.join(trash_dir, str(os.getuid()), "files", gen_ext_vol[1])) is True
op.exists(op.join(trashDir, str(os.getuid()), "files", testExtVol[1])) is True
)
assert ( assert (
op.exists( op.exists(
op.join(trashDir, str(os.getuid()), "info", testExtVol[1] + ".trashinfo",) op.join(
trash_dir,
str(os.getuid()),
"info",
gen_ext_vol[1] + INFO_SUFFIX,
)
) )
is True 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()
cfg.read(op.join(trashDir, str(os.getuid()), "info", testExtVol[1] + ".trashinfo")) cfg.read(op.join(trash_dir, str(os.getuid()), "info", gen_ext_vol[1] + INFO_SUFFIX))
assert (testExtVol[1] == cfg.get("Trash Info", "Path", raw=True)) is True assert (gen_ext_vol[1] == cfg.get("Trash Info", "Path", raw=True)) is True
def test_trash_topdir_fallback(testExtVol): def test_trash_topdir_fallback(gen_ext_vol):
s2t(testExtVol[2]) s2t(gen_ext_vol[2])
assert op.exists(testExtVol[2]) is False assert op.exists(gen_ext_vol[2]) is False
assert ( assert (
op.exists( op.exists(
op.join( op.join(
testExtVol[0].trashTopdir, gen_ext_vol[0].trash_topdir,
".Trash-" + str(os.getuid()), ".Trash-" + str(os.getuid()),
"files", "files",
testExtVol[1], gen_ext_vol[1],
) )
) )
is True is True
) )
def test_trash_topdir_failure(testExtVol): def test_trash_topdir_failure(gen_ext_vol):
os.chmod(testExtVol[0].trashTopdir, 0o500) # not writable to induce the exception os.chmod(gen_ext_vol[0].trash_topdir, 0o500) # not writable to induce the exception
pytest.raises(TrashPermissionError, s2t, [testExtVol[2]]) pytest.raises(TrashPermissionError, s2t, [gen_ext_vol[2]])
os.chmod(testExtVol[0].trashTopdir, 0o700) # writable to allow deletion os.chmod(gen_ext_vol[0].trash_topdir, 0o700) # writable to allow deletion
def test_trash_symlink(testExtVol): def test_trash_symlink(gen_ext_vol):
# Use mktemp (race conditioney but no symlink equivalent) # Generating a random uuid named path for symlink
# Since is_parent uses realpath(), and our getdev uses is_parent, sl_dir = op.join(op.expanduser("~"), "s2t_" + str(uuid.uuid4()))
# this should work os.mkdir(op.join(gen_ext_vol[0].trash_topdir, "subdir"), 0o700)
slDir = mktemp(prefix="s2t", dir=op.expanduser("~")) file_path = op.join(gen_ext_vol[0].trash_topdir, "subdir", gen_ext_vol[1])
os.mkdir(op.join(testExtVol[0].trashTopdir, "subdir"), 0o700) touch(file_path)
filePath = op.join(testExtVol[0].trashTopdir, "subdir", testExtVol[1]) os.symlink(op.join(gen_ext_vol[0].trash_topdir, "subdir"), sl_dir)
touch(filePath) s2t(op.join(sl_dir, gen_ext_vol[1]))
os.symlink(op.join(testExtVol[0].trashTopdir, "subdir"), slDir) assert op.exists(file_path) is False
s2t(op.join(slDir, testExtVol[1]))
assert op.exists(filePath) is False
assert ( assert (
op.exists( op.exists(
op.join( op.join(
testExtVol[0].trashTopdir, gen_ext_vol[0].trash_topdir,
".Trash-" + str(os.getuid()), ".Trash-" + str(os.getuid()),
"files", "files",
testExtVol[1], gen_ext_vol[1],
) )
) )
is True is True
) )
os.remove(slDir) os.remove(sl_dir)

View File

@@ -9,8 +9,8 @@ from send2trash import send2trash as s2t
# import the two versions as well as the "automatic" version # import the two versions as well as the "automatic" version
if sys.platform == "win32": if sys.platform == "win32":
from send2trash.plat_win_modern import send2trash as s2t_modern from send2trash.win.modern import send2trash as s2t_modern
from send2trash.plat_win_legacy import send2trash as s2t_legacy from send2trash.win.legacy import send2trash as s2t_legacy
else: else:
pytest.skip("Skipping windows-only tests", allow_module_level=True) pytest.skip("Skipping windows-only tests", allow_module_level=True)
@@ -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)
try:
shutil.rmtree(dirname, ignore_errors=True) shutil.rmtree(dirname, ignore_errors=True)
except TypeError:
pass
@pytest.fixture @pytest.fixture
@@ -201,3 +204,17 @@ def test_trash_long_multifile_legacy(longfiles):
# ) # )
# def test_trash_long_folder_legacy(self): # def test_trash_long_folder_legacy(self):
# self._trash_folder(s2t_legacy) # self._trash_folder(s2t_legacy)
def test_trash_nothing_legacy():
try:
s2t_legacy([])
except Exception as ex:
assert False, "Exception thrown when trashing nothing: {}".format(ex)
def test_trash_nothing_modern():
try:
s2t_modern([])
except Exception as ex:
assert False, "Exception thrown when trashing nothing: {}".format(ex)

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