New Upstream Release - send2trash
Ready changes
Summary
Merged new upstream version: 1.8.2 (was: 1.8.1~b0).
Diff
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index a8a4f5b..8cbcc5f 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -13,12 +13,12 @@ name: "CodeQL"
on:
push:
- branches: [ master ]
+ branches: [master]
pull_request:
# The branches below must be a subset of the branches above
- branches: [ master ]
+ branches: [master]
schedule:
- - cron: '25 5 * * 4'
+ - cron: "25 5 * * 4"
jobs:
analyze:
@@ -32,40 +32,40 @@ jobs:
strategy:
fail-fast: false
matrix:
- language: [ 'python' ]
+ 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
+ - name: Checkout repository
+ uses: actions/checkout@v3
- # 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
+ # 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@v1
+ # 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
+ # ℹ️ 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
+ # ✏️ 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
+ #- run: |
+ # make bootstrap
+ # make release
- - name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v1
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v2
diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml
index 3731291..a65bb09 100644
--- a/.github/workflows/default.yml
+++ b/.github/workflows/default.yml
@@ -4,26 +4,26 @@ name: Default CI/CD
on:
push:
- branches: [ master ]
+ branches: [master]
pull_request:
- branches: [ master ]
+ 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 .
+ - 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 }}
@@ -31,50 +31,52 @@ jobs:
matrix:
include:
- os: ubuntu-latest
- python-version: 3.10.0-alpha - 3.10.0
+ 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-latest
+ - os: ubuntu-20.04
python-version: 3.6
- - os: ubuntu-latest
+ - os: ubuntu-20.04
python-version: 3.5
- os: ubuntu-latest
python-version: 2.7
# - os: macos-latest
- # python-version: 3.9
+ # python-version: 3.11
# - os: macos-latest
# python-version: 3.8
# - os: macos-latest
# python-version: 2.7
- os: windows-latest
- python-version: 3.9
+ python-version: 3.11
- 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
+ - 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
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 1434c69..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,19 +0,0 @@
-language: python
-matrix:
- include:
- - python: "2.7"
- arch: ppc64le
- - python: "3.6"
- arch: ppc64le
- - python: "3.7"
- arch: ppc64le
- - python: "3.8"
- arch: ppc64le
- - python: "3.9"
- arch: ppc64le
-install:
- - python -m pip install tox
-before_script:
- - export TOXENV=$(echo py$TRAVIS_PYTHON_VERSION | tr -d .)
-script:
- - python -m tox
diff --git a/CHANGES.rst b/CHANGES.rst
index aa0b627..2093755 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,9 +1,15 @@
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)
+* Add fallback to HOMETRASH when cross device errors happen in plat_other (#26, #41, #63)
Version 1.8.0 -- 2021/08/08
---------------------------
diff --git a/debian/changelog b/debian/changelog
index df65853..fda8de0 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+send2trash (1.8.2-1) UNRELEASED; urgency=low
+
+ * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk> Thu, 25 May 2023 04:50:32 -0000
+
send2trash (1.8.1~b0-2) unstable; urgency=medium
* Migrate debian/watch away from GitHub releases page
diff --git a/send2trash/__init__.py b/send2trash/__init__.py
index 4c3f7e9..d09faa3 100644
--- a/send2trash/__init__.py
+++ b/send2trash/__init__.py
@@ -6,16 +6,16 @@
import sys
-from .exceptions import TrashPermissionError # noqa: F401
+from send2trash.exceptions import TrashPermissionError # noqa: F401
if sys.platform == "darwin":
- from .plat_osx import send2trash
+ from send2trash.mac import send2trash
elif sys.platform == "win32":
- from .plat_win import send2trash
+ from send2trash.win import send2trash
else:
try:
# If we can use gio, let's use it
- from .plat_gio import send2trash
+ from send2trash.plat_gio import send2trash
except ImportError:
# 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
diff --git a/send2trash/exceptions.py b/send2trash/exceptions.py
index b61c645..9e82766 100644
--- a/send2trash/exceptions.py
+++ b/send2trash/exceptions.py
@@ -1,5 +1,5 @@
import errno
-from .compat import PY3
+from send2trash.compat import PY3
if PY3:
_permission_error = PermissionError # noqa: F821
diff --git a/send2trash/plat_osx.py b/send2trash/mac/__init__.py
similarity index 79%
rename from send2trash/plat_osx.py
rename to send2trash/mac/__init__.py
index 9ef4d6c..beff611 100644
--- a/send2trash/plat_osx.py
+++ b/send2trash/mac/__init__.py
@@ -11,10 +11,10 @@ from sys import version_info
macos_ver = tuple(int(part) for part in mac_ver()[0].split("."))
if version_info >= (3, 6) and macos_ver >= (10, 9):
try:
- from .plat_osx_pyobjc import send2trash
+ from send2trash.mac.modern import send2trash
except ImportError:
# Try to fall back to ctypes version, although likely problematic still
- from .plat_osx_ctypes import send2trash
+ from send2trash.mac.legacy import send2trash
else:
# Just use the old version otherwise
- from .plat_osx_ctypes import send2trash # noqa: F401
+ from send2trash.mac.legacy import send2trash # noqa: F401
diff --git a/send2trash/plat_osx_ctypes.py b/send2trash/mac/legacy.py
similarity index 95%
rename from send2trash/plat_osx_ctypes.py
rename to send2trash/mac/legacy.py
index 4b8c2b3..82f43d9 100644
--- a/send2trash/plat_osx_ctypes.py
+++ b/send2trash/mac/legacy.py
@@ -9,8 +9,8 @@ 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
+from send2trash.compat import binary_type
+from send2trash.util import preprocess_paths
Foundation = cdll.LoadLibrary(find_library("Foundation"))
CoreServices = cdll.LoadLibrary(find_library("CoreServices"))
diff --git a/send2trash/plat_osx_pyobjc.py b/send2trash/mac/modern.py
similarity index 91%
rename from send2trash/plat_osx_pyobjc.py
rename to send2trash/mac/modern.py
index 8694444..6098d5a 100644
--- a/send2trash/plat_osx_pyobjc.py
+++ b/send2trash/mac/modern.py
@@ -5,8 +5,8 @@
# http://www.hardcoded.net/licenses/bsd_license
from Foundation import NSFileManager, NSURL
-from .compat import text_type
-from .util import preprocess_paths
+from send2trash.compat import text_type
+from send2trash.util import preprocess_paths
def check_op_result(op_result):
diff --git a/send2trash/plat_gio.py b/send2trash/plat_gio.py
index 0b11c8d..258e4ef 100644
--- a/send2trash/plat_gio.py
+++ b/send2trash/plat_gio.py
@@ -5,8 +5,8 @@
# http://www.hardcoded.net/licenses/bsd_license
from gi.repository import GObject, Gio
-from .exceptions import TrashPermissionError
-from .util import preprocess_paths
+from send2trash.exceptions import TrashPermissionError
+from send2trash.util import preprocess_paths
def send2trash(paths):
diff --git a/send2trash/plat_other.py b/send2trash/plat_other.py
index 2222310..517e2a0 100644
--- a/send2trash/plat_other.py
+++ b/send2trash/plat_other.py
@@ -30,9 +30,9 @@ except ImportError:
# Python 2
from urllib import quote
-from .compat import text_type, environb
-from .util import preprocess_paths
-from .exceptions import TrashPermissionError
+from send2trash.compat import text_type, environb
+from send2trash.util import preprocess_paths
+from send2trash.exceptions import TrashPermissionError
try:
fsencode = os.fsencode # Python 3
@@ -182,26 +182,23 @@ def send2trash(paths):
path_b = fsencode(path)
elif isinstance(path, bytes):
path_b = path
- elif hasattr(path, "__fspath__"):
- # Python 3.6 PathLike protocol
- return send2trash(path.__fspath__())
else:
raise TypeError("str, bytes or PathLike expected, not %r" % type(path))
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
# it, before starting the trashing operation itself. [2]
if not os.access(path_b, os.W_OK):
- raise OSError("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)
+ raise OSError(errno.EACCES, "Permission denied: %s" % path)
+ path_dev = get_dev(path_b)
# 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.
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:
topdir = XDG_DATA_HOME
dest_trash = HOMETRASH_B
diff --git a/send2trash/IFileOperationProgressSink.py b/send2trash/win/IFileOperationProgressSink.py
similarity index 52%
rename from send2trash/IFileOperationProgressSink.py
rename to send2trash/win/IFileOperationProgressSink.py
index c8f5234..c8702df 100644
--- a/send2trash/IFileOperationProgressSink.py
+++ b/send2trash/win/IFileOperationProgressSink.py
@@ -37,54 +37,10 @@ class FileOperationProgressSink(DesignatedWrapPolicy):
# 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 PostDeleteItem(self, flags, item, hr_delete, newly_created):
+ if newly_created:
+ self.newItem = newly_created.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():
+def create_sink():
return pythoncom.WrapObject(FileOperationProgressSink(), shell.IID_IFileOperationProgressSink)
diff --git a/send2trash/plat_win.py b/send2trash/win/__init__.py
similarity index 78%
rename from send2trash/plat_win.py
rename to send2trash/win/__init__.py
index 64ae85c..4a06123 100644
--- a/send2trash/plat_win.py
+++ b/send2trash/win/__init__.py
@@ -11,10 +11,10 @@ from platform import version
if int(version().split(".", 1)[0]) >= 6:
try:
# Attempt to use pywin32 to use IFileOperation
- from .plat_win_modern import send2trash
+ from send2trash.win.modern import send2trash
except ImportError:
# use SHFileOperation as fallback
- from .plat_win_legacy import send2trash
+ from send2trash.win.legacy import send2trash
else:
# use SHFileOperation as fallback
- from .plat_win_legacy import send2trash # noqa: F401
+ from send2trash.win.legacy import send2trash # noqa: F401
diff --git a/send2trash/plat_win_legacy.py b/send2trash/win/legacy.py
similarity index 68%
rename from send2trash/plat_win_legacy.py
rename to send2trash/win/legacy.py
index 137c592..9abf5bc 100644
--- a/send2trash/plat_win_legacy.py
+++ b/send2trash/win/legacy.py
@@ -6,8 +6,9 @@
from __future__ import unicode_literals
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 (
windll,
@@ -53,6 +54,41 @@ FOF_ALLOWUNDO = 64
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):
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)
@@ -104,6 +140,8 @@ def get_short_path_name(long_name):
def send2trash(paths):
paths = preprocess_paths(paths)
+ if not paths:
+ return
# convert data type
paths = [text_type(path, "mbcs") if not isinstance(path, text_type) else path for path in paths]
# convert to full paths
@@ -141,4 +179,5 @@ def send2trash(paths):
fileop.lpszProgressTitle = None
result = SHFileOperationW(byref(fileop))
if result:
- raise WindowsError(result, FormatError(result), paths)
+ error = convert_sh_file_opt_result(result)
+ raise WindowsError(None, FormatError(error), paths, error)
diff --git a/send2trash/plat_win_modern.py b/send2trash/win/modern.py
similarity index 88%
rename from send2trash/plat_win_modern.py
rename to send2trash/win/modern.py
index 5f30713..7927a89 100644
--- a/send2trash/plat_win_modern.py
+++ b/send2trash/win/modern.py
@@ -6,17 +6,19 @@
from __future__ import unicode_literals
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 platform import version
import pythoncom
import pywintypes
from win32com.shell import shell, shellcon
-from .IFileOperationProgressSink import CreateSink
+from send2trash.win.IFileOperationProgressSink import create_sink
def send2trash(paths):
paths = preprocess_paths(paths)
+ if not paths:
+ return
# convert data type
paths = [text_type(path, "mbcs") if not isinstance(path, text_type) else path for path in paths]
# convert to full paths
@@ -27,7 +29,10 @@ def send2trash(paths):
pythoncom.CoInitialize()
# create instance of file operation object
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
flags = shellcon.FOF_NOCONFIRMATION | shellcon.FOF_NOERRORUI | shellcon.FOF_SILENT | shellcon.FOFX_EARLYFAILURE
@@ -42,7 +47,7 @@ def send2trash(paths):
# actually try to perform the operation, this section may throw a
# pywintypes.com_error which does not seem to create as nice of an
# error as OSError so wrapping with try to convert
- sink = CreateSink()
+ sink = create_sink()
try:
for path in paths:
item = shell.SHCreateItemFromParsingName(path, None, shell.IID_IShellItem)
diff --git a/setup.cfg b/setup.cfg
index 91fd7aa..78316a2 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
[metadata]
name = Send2Trash
-version = 1.8.1b0
+version = 1.8.2
url = https://github.com/arsenetar/send2trash
project_urls =
Bug Reports = https://github.com/arsenetar/send2trash/issues
@@ -26,16 +26,23 @@ classifiers =
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 = send2trash
+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"
+win32 =
+ pywin32; sys_platform == "win32"
+objc =
+ pyobjc-framework-Cocoa; sys_platform == "darwin"
nativeLib =
pywin32; sys_platform == "win32"
pyobjc-framework-Cocoa; sys_platform == "darwin"
diff --git a/tests/test_plat_other.py b/tests/test_plat_other.py
index 6a796fa..331e3ef 100644
--- a/tests/test_plat_other.py
+++ b/tests/test_plat_other.py
@@ -13,14 +13,16 @@ except ImportError:
# py2
from ConfigParser import ConfigParser # noqa: F401
-from tempfile import mkdtemp, NamedTemporaryFile, mktemp
+from tempfile import mkdtemp, NamedTemporaryFile
import shutil
import stat
+import uuid
if sys.platform != "win32":
import send2trash.plat_other
from send2trash.plat_other import send2trash as s2t
+ INFO_SUFFIX = send2trash.plat_other.INFO_SUFFIX.decode()
HOMETRASH = send2trash.plat_other.HOMETRASH
else:
pytest.skip("Skipping non-windows tests", allow_module_level=True)
@@ -38,7 +40,7 @@ def testfile():
# 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, "info", name + ".trashinfo"))
+ os.remove(op.join(HOMETRASH, "info", name + INFO_SUFFIX))
if op.exists(file.name):
os.remove(file.name)
@@ -48,7 +50,9 @@ def testfiles():
files = list(
map(
lambda index: NamedTemporaryFile(
- dir=op.expanduser("~"), prefix="send2trash_test{}".format(index), delete=False,
+ dir=op.expanduser("~"),
+ prefix="send2trash_test{}".format(index),
+ delete=False,
),
range(10),
)
@@ -58,7 +62,7 @@ def testfiles():
yield 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, "info", filename + ".trashinfo")) for filename in filenames]
+ [os.remove(op.join(HOMETRASH, "info", filename + INFO_SUFFIX)) for filename in filenames]
def test_trash(testfile):
@@ -84,52 +88,53 @@ def _filesys_enc():
@pytest.fixture
-def testUnicodefile():
+def gen_unicode_file():
name = u"send2trash_tést1"
file = op.join(op.expanduser(b"~"), name.encode("utf-8"))
touch(file)
assert op.exists(file) is True
yield file
# Cleanup trash files on supported platforms
- if sys.platform != "win32":
- # 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, "info", name + ".trashinfo"))
+ if sys.platform != "win32" and op.exists(op.join(HOMETRASH, "files", name)):
+ os.remove(op.join(HOMETRASH, "files", name))
+ os.remove(op.join(HOMETRASH, "info", name + INFO_SUFFIX))
if op.exists(file):
os.remove(file)
@pytest.mark.skipif(_filesys_enc() == "ascii", reason="Requires Unicode filesystem")
-def test_trash_bytes(testUnicodefile):
- s2t(testUnicodefile)
- assert not op.exists(testUnicodefile)
+def test_trash_bytes(gen_unicode_file):
+ s2t(gen_unicode_file)
+ assert not op.exists(gen_unicode_file)
@pytest.mark.skipif(_filesys_enc() == "ascii", reason="Requires Unicode filesystem")
-def test_trash_unicode(testUnicodefile):
- s2t(testUnicodefile.decode(sys.getfilesystemencoding()))
- assert not op.exists(testUnicodefile)
+def test_trash_unicode(gen_unicode_file):
+ s2t(gen_unicode_file.decode(sys.getfilesystemencoding()))
+ assert not op.exists(gen_unicode_file)
class ExtVol:
def __init__(self, path):
- self.trashTopdir = path
+ self.trash_topdir = path
if PY3:
- self.trashTopdir_b = os.fsencode(self.trashTopdir)
+ self.trash_topdir_b = os.fsencode(self.trash_topdir)
else:
- self.trashTopdir_b = self.trashTopdir
+ self.trash_topdir_b = self.trash_topdir
def s_getdev(path):
from send2trash.plat_other import is_parent
st = os.lstat(path)
- if is_parent(self.trashTopdir, path):
+ if is_parent(self.trash_topdir, path):
return "dev"
return st.st_dev
def s_ismount(path):
- if op.realpath(path) in (op.realpath(self.trashTopdir), op.realpath(self.trashTopdir_b),):
+ if op.realpath(path) in (
+ op.realpath(self.trash_topdir),
+ op.realpath(self.trash_topdir_b),
+ ):
return True
return old_ismount(path)
@@ -141,58 +146,86 @@ class ExtVol:
def cleanup(self):
send2trash.plat_other.get_dev = self.old_getdev
send2trash.plat_other.os.path.ismount = self.old_ismount
- shutil.rmtree(self.trashTopdir)
+ shutil.rmtree(self.trash_topdir)
@pytest.fixture
-def testExtVol():
- trashTopdir = mkdtemp(prefix="s2t")
- volume = ExtVol(trashTopdir)
- fileName = "test.txt"
- filePath = op.join(volume.trashTopdir, fileName)
- touch(filePath)
- assert op.exists(filePath) is True
- yield volume, fileName, filePath
+def gen_ext_vol():
+ trash_topdir = mkdtemp(prefix="s2t")
+ volume = ExtVol(trash_topdir)
+ file_name = "test.txt"
+ file_path = op.join(volume.trash_topdir, file_name)
+ touch(file_path)
+ assert op.exists(file_path) is True
+ yield volume, file_name, file_path
volume.cleanup()
-def test_trash_topdir(testExtVol):
- trashDir = op.join(testExtVol[0].trashTopdir, ".Trash")
- os.mkdir(trashDir, 0o777 | stat.S_ISVTX)
-
- s2t(testExtVol[2])
- assert op.exists(testExtVol[2]) is False
- assert 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
+def test_trash_topdir(gen_ext_vol):
+ trash_dir = op.join(gen_ext_vol[0].trash_topdir, ".Trash")
+ os.mkdir(trash_dir, 0o777 | stat.S_ISVTX)
+
+ s2t(gen_ext_vol[2])
+ assert op.exists(gen_ext_vol[2]) is False
+ assert op.exists(op.join(trash_dir, str(os.getuid()), "files", gen_ext_vol[1])) is True
+ assert (
+ op.exists(
+ op.join(
+ trash_dir,
+ str(os.getuid()),
+ "info",
+ gen_ext_vol[1] + INFO_SUFFIX,
+ )
+ )
+ is True
+ )
# info relative path (if another test is added, with the same fileName/Path,
# then it gets renamed etc.)
cfg = ConfigParser()
- cfg.read(op.join(trashDir, str(os.getuid()), "info", testExtVol[1] + ".trashinfo"))
- assert (testExtVol[1] == cfg.get("Trash Info", "Path", raw=True)) is True
-
-
-def test_trash_topdir_fallback(testExtVol):
- s2t(testExtVol[2])
- assert op.exists(testExtVol[2]) is False
- assert op.exists(op.join(testExtVol[0].trashTopdir, ".Trash-" + str(os.getuid()), "files", testExtVol[1],)) is True
-
-
-def test_trash_topdir_failure(testExtVol):
- os.chmod(testExtVol[0].trashTopdir, 0o500) # not writable to induce the exception
- pytest.raises(TrashPermissionError, s2t, [testExtVol[2]])
- os.chmod(testExtVol[0].trashTopdir, 0o700) # writable to allow deletion
-
-
-def test_trash_symlink(testExtVol):
- # Use mktemp (race conditioney but no symlink equivalent)
- # Since is_parent uses realpath(), and our getdev uses is_parent,
- # this should work
- slDir = mktemp(prefix="s2t", dir=op.expanduser("~"))
- os.mkdir(op.join(testExtVol[0].trashTopdir, "subdir"), 0o700)
- filePath = op.join(testExtVol[0].trashTopdir, "subdir", testExtVol[1])
- touch(filePath)
- os.symlink(op.join(testExtVol[0].trashTopdir, "subdir"), slDir)
- s2t(op.join(slDir, testExtVol[1]))
- assert op.exists(filePath) is False
- assert op.exists(op.join(testExtVol[0].trashTopdir, ".Trash-" + str(os.getuid()), "files", testExtVol[1],)) is True
- os.remove(slDir)
+ cfg.read(op.join(trash_dir, str(os.getuid()), "info", gen_ext_vol[1] + INFO_SUFFIX))
+ assert (gen_ext_vol[1] == cfg.get("Trash Info", "Path", raw=True)) is True
+
+
+def test_trash_topdir_fallback(gen_ext_vol):
+ s2t(gen_ext_vol[2])
+ assert op.exists(gen_ext_vol[2]) is False
+ assert (
+ op.exists(
+ op.join(
+ gen_ext_vol[0].trash_topdir,
+ ".Trash-" + str(os.getuid()),
+ "files",
+ gen_ext_vol[1],
+ )
+ )
+ is True
+ )
+
+
+def test_trash_topdir_failure(gen_ext_vol):
+ os.chmod(gen_ext_vol[0].trash_topdir, 0o500) # not writable to induce the exception
+ pytest.raises(TrashPermissionError, s2t, [gen_ext_vol[2]])
+ os.chmod(gen_ext_vol[0].trash_topdir, 0o700) # writable to allow deletion
+
+
+def test_trash_symlink(gen_ext_vol):
+ # Generating a random uuid named path for symlink
+ sl_dir = op.join(op.expanduser("~"), "s2t_" + str(uuid.uuid4()))
+ os.mkdir(op.join(gen_ext_vol[0].trash_topdir, "subdir"), 0o700)
+ file_path = op.join(gen_ext_vol[0].trash_topdir, "subdir", gen_ext_vol[1])
+ touch(file_path)
+ os.symlink(op.join(gen_ext_vol[0].trash_topdir, "subdir"), sl_dir)
+ s2t(op.join(sl_dir, gen_ext_vol[1]))
+ assert op.exists(file_path) is False
+ assert (
+ op.exists(
+ op.join(
+ gen_ext_vol[0].trash_topdir,
+ ".Trash-" + str(os.getuid()),
+ "files",
+ gen_ext_vol[1],
+ )
+ )
+ is True
+ )
+ os.remove(sl_dir)
diff --git a/tests/test_plat_win.py b/tests/test_plat_win.py
index bd1ea3b..bf77437 100644
--- a/tests/test_plat_win.py
+++ b/tests/test_plat_win.py
@@ -9,8 +9,8 @@ from send2trash import send2trash as s2t
# import the two versions as well as the "automatic" version
if sys.platform == "win32":
- from send2trash.plat_win_modern import send2trash as s2t_modern
- from send2trash.plat_win_legacy import send2trash as s2t_legacy
+ from send2trash.win.modern import send2trash as s2t_modern
+ from send2trash.win.legacy import send2trash as s2t_legacy
else:
pytest.skip("Skipping windows-only tests", allow_module_level=True)
@@ -204,3 +204,17 @@ def test_trash_long_multifile_legacy(longfiles):
# )
# def test_trash_long_folder_legacy(self):
# 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)