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

32 Commits
1.8.2 ... 2.1.0

Author SHA1 Message Date
2109d6e518 doc: Update changelog for 2.1.0 2026-01-14 06:15:16 +00:00
Andrey Efremov
0b6b2fe5b0 Replace elif with else (#104) 2026-01-01 00:55:45 -06:00
9615b7e4f8 build: Move to pyproject.toml over setup.cfg
Convert to the more modern and preferred pyproject.toml configuration
for the project.

- Add the majority of the configuration to pyproject.toml.
- Remove setup.cfg as it is no longer needed.
- Update tox.ini to remove a lingering testenv for Python 2.7 which is
  no longer used.

NOTE: Manifest.in is left as the files it includes are outside of the
package directories.
2026-01-01 02:07:30 +00:00
34c0ddaa88 Merge pull request #106 from mgorny/sdist-conftest
Add `conftest.py` to source distribution
2025-12-31 19:06:04 -06:00
8cc111a446 ci: Update default workflow triggers 2026-01-01 01:01:42 +00:00
Michał Górny
f8a40143f6 Add conftest.py to source distribution
Fixes #105

Signed-off-by: Michał Górny <mgorny@gentoo.org>
2025-12-31 08:22:59 +01:00
5c4e6a81cd Merge pull request #103 from arsenetar/as/ci-lang-updates
feat: CI, Lanugage, lint, and misc cleanups
2025-12-30 22:06:40 -06:00
4e8d6c7e11 chore: Set version 2025-12-31 04:01:56 +00:00
2d07f76209 doc: Update CHANGES.rst for 2.0.0 2025-12-31 03:33:22 +00:00
a6539d146b fix: Update gio implementation to use GLib.Error instead of deprecated GObject.GError 2025-12-31 03:32:50 +00:00
2b20d606e2 doc: Update README to reflect python version support 2025-12-31 03:01:13 +00:00
06a9d37805 chore: More pylint cleanups, and python2 removals 2025-12-31 02:57:06 +00:00
8d96aa29df tests: Cleanup some pylint errors and share common fixture
- Cleanup some of the pylint erros in the tests
- Reorganize some of the tests functions and fixtures
- Move the one common fixture to conftest.py for sharing
2025-12-31 02:30:38 +00:00
0a4473d954 ci: Test python 3.9 on macOS instead of 3.8 as min version
PyObjC needs at least 3.9 now.
2025-12-31 01:34:12 +00:00
b23cba1ae2 chore: Fix lint error in test_plat_other.py 2025-12-31 01:32:01 +00:00
9465587899 chore: Update CI workflow versions add python 3.14
- Add Python 3.14
- Update GitHub Actions to latest versions
- Enable macOS in CI
2025-12-31 01:29:34 +00:00
b00bf8f8e0 Merge pull request #102 from gunSlaveUnit/fix/102-install-source-via-pip 2025-12-30 18:38:27 -06:00
gunSlaveUnit
fa9fee0442 Remove '>>>' from shell commands. 2025-12-27 15:39:46 +03:00
gunSlaveUnit
c91f2559da Fix source installation instructions.
- Replace outdated 'python setup.py install' with 'python -m pip install -e .'.
- Add virtual environment instructions for PEP 668 compatibility.
- Update the GitHub URL to HTTPS.
2025-12-27 15:23:33 +03:00
3f6c5b3088 Merge pull request #100 from denini08/mac-fix 2025-10-23 20:17:16 -05:00
denini08
1781b16fa8 test: Update trash topdir test for cross-platform compatibility 2025-09-24 15:26:49 -03:00
9a2c5bc690 chore: Update project configuration and ci for python version changes
- Drop support for Python 2
- Drop support for Python 3.7, 3.8 is new minimum
- Update tox to include newer python version and drop old ones
- Update GitHub action for python version changes, use standard python
  setup action
- Update GitHub action to use pinned action versions
- Update version to 2.0.0-dev
2025-08-06 05:27:33 +00:00
65bda6c7ca feat: Drop support for Python 2 and remove compatibility code
This removes support for Python 2, and drops most of the compatibility
code that was used to support both Python 2 and Python 3.
2025-08-06 05:27:32 +00:00
91d0698967 Merge pull request #91 from arsenetar/as/misc-fixes
chore: Upgrade version to 1.8.3, bump CI
2024-04-06 18:53:45 -05:00
5c47eb063c fix(ci): Use alternative setup-python to allow 2.7
The setup python action has removed 2.7, we want to keep it for the
moment.  Using an alternative action that supports 2.7.
2024-04-06 16:51:34 -07:00
c7a23884a9 chore: Upgrade version to 1.8.3, bump CI
- Upgrade version to 1.8.3, add changelog entry
- Fix minor flake8 error that can be ignored
- Update CI workflow to use newer actions and python versions
2024-04-06 16:36:23 -07:00
78fa300cac Merge pull request #90 from yogeshiitm/master
Fix bug when source and destination directories are on different file systems
2024-03-28 01:17:37 -05:00
Yogesh Agarwala
baeb9e59f9 Fix bug in send2trash: Use os.fsdecode() in shutil.move()
`shutil.move()` function expects string paths, not byte paths. This bug is leading to failure when src and dst are on on the different file system.
2024-03-23 05:37:21 +05:30
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
23 changed files with 391 additions and 478 deletions

View File

@@ -1,14 +1,3 @@
# 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" name: "CodeQL"
on: on:
@@ -33,39 +22,14 @@ jobs:
fail-fast: false fail-fast: false
matrix: 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: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v2 uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
with: with:
languages: ${{ matrix.language }} 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 - name: Autobuild
uses: github/codeql-action/autobuild@v2 uses: github/codeql-action/autobuild@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
# 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 - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2 uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9

View File

@@ -2,9 +2,9 @@
name: Default CI/CD name: Default CI/CD
on: on:
push: push:
branches: [master] branches: ["**"]
pull_request: pull_request:
branches: [master] branches: [master]
@@ -12,9 +12,9 @@ jobs:
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Python 3.x - name: Set up Python 3.x
uses: actions/setup-python@v3 uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with: with:
python-version: 3.x python-version: 3.x
- name: Install dependencies - name: Install dependencies
@@ -30,6 +30,12 @@ jobs:
strategy: strategy:
matrix: matrix:
include: include:
- os: ubuntu-latest
python-version: 3.14
- os: ubuntu-latest
python-version: 3.13
- os: ubuntu-latest
python-version: 3.12
- os: ubuntu-latest - os: ubuntu-latest
python-version: 3.11 python-version: 3.11
- os: ubuntu-latest - os: ubuntu-latest
@@ -38,31 +44,19 @@ jobs:
python-version: 3.9 python-version: 3.9
- os: ubuntu-latest - os: ubuntu-latest
python-version: 3.8 python-version: 3.8
- os: ubuntu-latest - os: macos-latest
python-version: 3.7 python-version: 3.14
- os: ubuntu-20.04 - os: macos-latest
python-version: 3.6 python-version: 3.9
- 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 - os: windows-latest
python-version: 3.11 python-version: 3.14
- os: windows-latest - os: windows-latest
python-version: 3.8 python-version: 3.8
- os: windows-latest
python-version: 2.7
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3 uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install dependencies - name: Install dependencies

View File

@@ -1,5 +1,22 @@
Changes Changes
======= =======
Version 2.1.0 -- 2026/01/14
---------------------------
* Add `conftest.py` to source distribution by @mgorny in https://github.com/arsenetar/send2trash/pull/106
* Replacing elif, which is always True, with else by @PalmtopTiger in https://github.com/arsenetar/send2trash/pull/104
* Migrate to pyproject.toml from setup.cfg
Version 2.0.0 -- 2025/12/30
---------------------------
* Drop suport for Python 2
* Fix `test_trash_topdir` failing on macOS by @denini08 in https://github.com/arsenetar/send2trash/pull/100
* Update source installation instructions by @gunSlaveUnit in https://github.com/arsenetar/send2trash/pull/102
* Update gio implemenation, should fix #5 by no longer using deprecated GObject.GError
Version 1.8.3 -- 2024/04/06
---------------------------
* Add support for any iterable type as input by @PalmtopTiger in https://github.com/arsenetar/send2trash/pull/88
* fix: Use os.fsdecode() for arguments to shutil.move() by @yogeshiitm in https://github.com/arsenetar/send2trash/pull/90
Version 1.8.2 -- 2023/04/27 Version 1.8.2 -- 2023/04/27
--------------------------- ---------------------------

View File

@@ -1 +1 @@
include CHANGES.rst LICENSE include CHANGES.rst LICENSE tests/conftest.py

View File

@@ -11,7 +11,7 @@ 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.
Send2Trash supports Python 2.7 and up (Python 3 is supported). Send2Trash supports Python 3 (versions before 2.x support Python 2).
Status: Additional Help Welcome Status: Additional Help Welcome
------------------------------- -------------------------------
@@ -30,9 +30,23 @@ To install with pywin32 or pyobjc required specify the extra `nativeLib`:
python -m pip install -U send2trash[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 download the source from https://github.com/arsenetar/send2trash and install it with:
>>> python setup.py install python -m pip install -e .
On systems where Python enforces PEP 668 (e.g., recent Linux distributions),
installing packages into the system Python may be restricted.
Use a virtual environment:
python -m venv .venv
GNU/Linux / macOS:
source .venv/bin/activate
Windows:
.venv\Scripts\activate
Usage Usage
----- -----
@@ -44,7 +58,7 @@ Usage
On Freedesktop platforms (Linux, BSD, etc.), you may not be able to efficiently On Freedesktop platforms (Linux, BSD, etc.), you may not be able to efficiently
trash some files. In these cases, an exception ``send2trash.TrashPermissionError`` trash some files. In these cases, an exception ``send2trash.TrashPermissionError``
is raised, so that the application can handle this case. This inherits from is raised, so that the application can handle this case. This inherits from
``PermissionError`` (``OSError`` on Python 2). Specifically, this affects ``PermissionError``. Specifically, this affects
files on a different device to the user's home directory, where the root of the files on a different device to the user's home directory, where the root of the
device does not have a ``.Trash`` directory, and we don't have permission to device does not have a ``.Trash`` directory, and we don't have permission to
create a ``.Trash-$UID`` directory. create a ``.Trash-$UID`` directory.

View File

@@ -1,6 +1,54 @@
[build-system] [build-system]
requires = ["setuptools >= 40.6.0", "wheel"] requires = ["setuptools >= 75.3.1"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project]
name = "Send2Trash"
version = "2.1.0"
description = "Send file to trash natively under Mac OS X, Windows and Linux"
readme = "README.rst"
license = "BSD-3-Clause"
license-files = ["LICENSE"]
authors = [
{name = "Andrew Senetar", email = "arsenetar@voltaicideas.net"},
]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Operating System :: MacOS :: MacOS X",
"Operating System :: Microsoft :: Windows",
"Operating System :: POSIX",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Desktop Environment :: File Managers"
]
requires-python = ">=3.8"
[project.urls]
Homepage = "https://github.com/arsenetar/send2trash"
Repository = "https://github.com/arsenetar/send2trash.git"
Issues = "https://github.com/arsenetar/send2trash/issues"
[project.scripts]
send2trash = "send2trash.__main__:main"
[project.optional-dependencies]
test = [
"pytest >= 8",
]
nativelib = [
"pywin32 >= 305; sys_platform == 'win32'",
"pyobjc >= 9.0; sys_platform == 'darwin'",
]
[tool.black] [tool.black]
line-length = 120 line-length = 120
[tool.isort]
profile = "black"

View File

@@ -8,6 +8,9 @@ import sys
from send2trash.exceptions import TrashPermissionError # noqa: F401 from send2trash.exceptions import TrashPermissionError # noqa: F401
if sys.version_info[0] < 3:
raise RuntimeError("send2trash is only compatible with Python 3 and above (use versions <= 1.8.3 for python 2).")
if sys.platform == "darwin": if sys.platform == "darwin":
from send2trash.mac import send2trash from send2trash.mac import send2trash
elif sys.platform == "win32": elif sys.platform == "win32":

View File

@@ -12,6 +12,9 @@ import sys
from argparse import ArgumentParser from argparse import ArgumentParser
from send2trash import send2trash from send2trash import send2trash
if sys.version_info[0] < 3:
raise RuntimeError("send2trash is only compatible with Python 3 and above (use versions <= 1.8.3 for python 2).")
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")

View File

@@ -1,20 +0,0 @@
# 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
import sys
import os
PY3 = sys.version_info[0] >= 3
if PY3:
text_type = str
binary_type = bytes
if os.supports_bytes_environ:
# environb will be unset under Windows, but then again we're not supposed to use it.
environb = os.environb
else:
text_type = unicode # noqa: F821
binary_type = str
environb = os.environ

View File

@@ -1,13 +1,7 @@
import errno import errno
from send2trash.compat import PY3
if PY3:
_permission_error = PermissionError # noqa: F821
else:
_permission_error = OSError
class TrashPermissionError(_permission_error): class TrashPermissionError(PermissionError):
"""A permission error specific to a trash directory. """A permission error specific to a trash directory.
Raising this error indicates that permissions prevent us efficiently Raising this error indicates that permissions prevent us efficiently
@@ -23,4 +17,4 @@ class TrashPermissionError(_permission_error):
""" """
def __init__(self, filename): def __init__(self, filename):
_permission_error.__init__(self, errno.EACCES, "Permission denied", filename) PermissionError.__init__(self, errno.EACCES, "Permission denied", filename)

View File

@@ -9,7 +9,6 @@ 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 send2trash.compat import binary_type
from send2trash.util import preprocess_paths from send2trash.util import preprocess_paths
Foundation = cdll.LoadLibrary(find_library("Foundation")) Foundation = cdll.LoadLibrary(find_library("Foundation"))
@@ -42,7 +41,7 @@ def check_op_result(op_result):
def send2trash(paths): def send2trash(paths):
paths = preprocess_paths(paths) paths = preprocess_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, bytes) 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,7 +5,6 @@
# 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 send2trash.compat import text_type
from send2trash.util import preprocess_paths from send2trash.util import preprocess_paths
@@ -18,7 +17,7 @@ def check_op_result(op_result):
def send2trash(paths): def send2trash(paths):
paths = preprocess_paths(paths) paths = preprocess_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, str) 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

@@ -4,7 +4,7 @@
# 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 gi.repository import GObject, Gio from gi.repository import Gio, GLib
from send2trash.exceptions import TrashPermissionError from send2trash.exceptions import TrashPermissionError
from send2trash.util import preprocess_paths from send2trash.util import preprocess_paths
@@ -15,9 +15,9 @@ def send2trash(paths):
try: try:
f = Gio.File.new_for_path(path) f = Gio.File.new_for_path(path)
f.trash(cancellable=None) f.trash(cancellable=None)
except GObject.GError as e: except GLib.Error as e:
if e.code == Gio.IOErrorEnum.NOT_SUPPORTED: if e.code == Gio.IOErrorEnum.NOT_SUPPORTED:
# We get here if we can't create a trash directory on the same # We get here if we can't create a trash directory on the same
# device. I don't know if other errors can result in NOT_SUPPORTED. # device. I don't know if other errors can result in NOT_SUPPORTED.
raise TrashPermissionError("") raise TrashPermissionError("") from e
raise OSError(e.message) raise OSError(e.message) from e

View File

@@ -17,58 +17,36 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import errno import errno
import sys
import os
import shutil import shutil
import os
import os.path as op import os.path as op
from datetime import datetime from datetime import datetime
import stat import stat
from urllib.parse import quote
try:
from urllib.parse import quote
except ImportError:
# Python 2
from urllib import quote
from send2trash.compat import text_type, environb
from send2trash.util import preprocess_paths from send2trash.util import preprocess_paths
from send2trash.exceptions import TrashPermissionError from send2trash.exceptions import TrashPermissionError
try:
fsencode = os.fsencode # Python 3
fsdecode = os.fsdecode
except AttributeError:
def fsencode(u): # Python 2
return u.encode(sys.getfilesystemencoding())
def fsdecode(b):
return b.decode(sys.getfilesystemencoding())
# The Python 3 versions are a bit smarter, handling surrogate escapes,
# but these should work in most cases.
FILES_DIR = b"files" FILES_DIR = b"files"
INFO_DIR = b"info" INFO_DIR = b"info"
INFO_SUFFIX = b".trashinfo" INFO_SUFFIX = b".trashinfo"
# Default of ~/.local/share [3] # Default of ~/.local/share [3]
XDG_DATA_HOME = op.expanduser(environb.get(b"XDG_DATA_HOME", b"~/.local/share")) XDG_DATA_HOME = op.expanduser(os.environb.get(b"XDG_DATA_HOME", b"~/.local/share"))
HOMETRASH_B = op.join(XDG_DATA_HOME, b"Trash") HOMETRASH_B = op.join(XDG_DATA_HOME, b"Trash")
HOMETRASH = fsdecode(HOMETRASH_B) HOMETRASH = os.fsdecode(HOMETRASH_B)
uid = os.getuid() uid = os.getuid()
TOPDIR_TRASH = b".Trash" TOPDIR_TRASH = b".Trash"
TOPDIR_FALLBACK = b".Trash-" + text_type(uid).encode("ascii") TOPDIR_FALLBACK = b".Trash-" + str(uid).encode("ascii")
def is_parent(parent, path): def is_parent(parent, path):
path = op.realpath(path) # In case it's a symlink path = op.realpath(path) # In case it's a symlink
if isinstance(path, text_type): if isinstance(path, str):
path = fsencode(path) path = os.fsencode(path)
parent = op.realpath(parent) parent = op.realpath(parent)
if isinstance(parent, text_type): if isinstance(parent, str):
parent = fsencode(parent) parent = os.fsencode(parent)
return path.startswith(parent) return path.startswith(parent)
@@ -90,34 +68,34 @@ def info_for(src, topdir):
return info return info
def check_create(dir): def check_create(folder):
# use 0700 for paths [3] # use 0700 for paths [3]
if not op.exists(dir): if not op.exists(folder):
os.makedirs(dir, 0o700) os.makedirs(folder, 0o700)
def trash_move(src, dst, topdir=None, cross_dev=False): def trash_move(src, dst, topdir=None, cross_dev=False):
filename = op.basename(src) file_name = op.basename(src)
filespath = op.join(dst, FILES_DIR) files_path = op.join(dst, FILES_DIR)
infopath = op.join(dst, INFO_DIR) info_path = op.join(dst, INFO_DIR)
base_name, ext = op.splitext(filename) base_name, ext = op.splitext(file_name)
counter = 0 counter = 0
destname = filename dest_name = file_name
while op.exists(op.join(filespath, destname)) or op.exists(op.join(infopath, destname + INFO_SUFFIX)): while op.exists(op.join(files_path, dest_name)) or op.exists(op.join(info_path, dest_name + INFO_SUFFIX)):
counter += 1 counter += 1
destname = base_name + b" " + text_type(counter).encode("ascii") + ext dest_name = base_name + b" " + str(counter).encode("ascii") + ext
check_create(filespath) check_create(files_path)
check_create(infopath) check_create(info_path)
with open(op.join(infopath, destname + INFO_SUFFIX), "w") as f: with open(op.join(info_path, dest_name + INFO_SUFFIX), "w") as f:
f.write(info_for(src, topdir)) f.write(info_for(src, topdir))
destpath = op.join(filespath, destname) dest_path = op.join(files_path, dest_name)
if cross_dev: if cross_dev:
shutil.move(src, destpath) shutil.move(os.fsdecode(src), os.fsdecode(dest_path))
else: else:
os.rename(src, destpath) os.rename(src, dest_path)
def find_mount_point(path): def find_mount_point(path):
@@ -139,10 +117,10 @@ def find_ext_volume_global_trash(volume_root):
mode = os.lstat(trash_dir).st_mode mode = os.lstat(trash_dir).st_mode
# vol/.Trash must be a directory, cannot be a symlink, and must have the # vol/.Trash must be a directory, cannot be a symlink, and must have the
# sticky bit set. # sticky bit set.
if not op.isdir(trash_dir) or op.islink(trash_dir) or not (mode & stat.S_ISVTX): if not op.isdir(trash_dir) or op.islink(trash_dir) or not mode & stat.S_ISVTX:
return None return None
trash_dir = op.join(trash_dir, text_type(uid).encode("ascii")) trash_dir = op.join(trash_dir, str(uid).encode("ascii"))
try: try:
check_create(trash_dir) check_create(trash_dir)
except OSError: except OSError:
@@ -158,7 +136,7 @@ def find_ext_volume_fallback_trash(volume_root):
check_create(trash_dir) check_create(trash_dir)
except OSError as e: except OSError as e:
if e.errno == errno.EACCES: if e.errno == errno.EACCES:
raise TrashPermissionError(e.filename) raise TrashPermissionError(e.filename) from e
raise raise
return trash_dir return trash_dir
@@ -178,19 +156,19 @@ def get_dev(path):
def send2trash(paths): def send2trash(paths):
paths = preprocess_paths(paths) paths = preprocess_paths(paths)
for path in paths: for path in paths:
if isinstance(path, text_type): if isinstance(path, str):
path_b = fsencode(path) path_b = os.fsencode(path)
elif isinstance(path, bytes): elif isinstance(path, bytes):
path_b = path path_b = path
else: else:
raise TypeError("str, bytes or PathLike expected, not %r" % type(path)) raise TypeError(f"str, bytes or PathLike expected, not {type(path)}")
if not op.exists(path_b): if not op.exists(path_b):
raise OSError(errno.ENOENT, "File not found: %s" % path) raise OSError(errno.ENOENT, f"File not found: {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(errno.EACCES, "Permission denied: %s" % path) raise OSError(errno.EACCES, f"Permission denied: {path}")
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
@@ -206,7 +184,7 @@ def send2trash(paths):
topdir = find_mount_point(path_b) topdir = find_mount_point(path_b)
trash_dev = get_dev(topdir) trash_dev = get_dev(topdir)
if trash_dev != path_dev: if trash_dev != path_dev:
raise OSError("Couldn't find mount point for %s" % path) raise OSError(f"Couldn't find mount point for {path}")
dest_trash = find_ext_volume_trash(topdir) dest_trash = find_ext_volume_trash(topdir)
try: try:
trash_move(path_b, dest_trash, topdir) trash_move(path_b, dest_trash, topdir)

View File

@@ -5,10 +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
import os
import collections.abc
def preprocess_paths(paths): def preprocess_paths(paths):
if not isinstance(paths, list): if isinstance(paths, collections.abc.Iterable) and not isinstance(paths, (str, bytes)):
paths = list(paths)
else:
paths = [paths] paths = [paths]
# Convert items such as pathlib paths to strings # Convert items such as pathlib paths to strings
paths = [path.__fspath__() if hasattr(path, "__fspath__") else path for path in paths] return [os.fspath(path) for path in paths]
return paths

View File

@@ -6,10 +6,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import os.path as op import os.path as op
from send2trash.compat import text_type
from send2trash.util import preprocess_paths
from ctypes import ( from ctypes import (
windll, windll,
Structure, Structure,
@@ -22,6 +18,8 @@ from ctypes import (
) )
from ctypes.wintypes import HWND, UINT, LPCWSTR, BOOL from ctypes.wintypes import HWND, UINT, LPCWSTR, BOOL
from send2trash.util import preprocess_paths
kernel32 = windll.kernel32 kernel32 = windll.kernel32
GetShortPathNameW = kernel32.GetShortPathNameW GetShortPathNameW = kernel32.GetShortPathNameW
@@ -143,7 +141,7 @@ def send2trash(paths):
if not paths: if not paths:
return return
# convert data type # convert data type
paths = [text_type(path, "mbcs") if not isinstance(path, text_type) else path for path in paths] paths = [str(path, "mbcs") if not isinstance(path, str) 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

@@ -6,12 +6,11 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import os.path as op import os.path as op
from send2trash.compat import text_type
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 send2trash.util import preprocess_paths
from send2trash.win.IFileOperationProgressSink import create_sink from send2trash.win.IFileOperationProgressSink import create_sink
@@ -20,7 +19,7 @@ def send2trash(paths):
if not paths: if not paths:
return return
# convert data type # convert data type
paths = [text_type(path, "mbcs") if not isinstance(path, text_type) else path for path in paths] paths = [str(path, "mbcs") if not isinstance(path, str) 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
@@ -60,7 +59,7 @@ def send2trash(paths):
except pywintypes.com_error as error: except pywintypes.com_error as error:
# 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) from error
finally: finally:
# Need to make sure we call this once fore every init # Need to make sure we call this once fore every init
pythoncom.CoUninitialize() pythoncom.CoUninitialize()

View File

@@ -1,52 +0,0 @@
[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

27
tests/conftest.py Normal file
View File

@@ -0,0 +1,27 @@
# encoding: utf-8
import sys
import os
from tempfile import NamedTemporaryFile
import pytest
# Only import HOMETRASH on supported platforms
if sys.platform != "win32":
from send2trash.plat_other import HOMETRASH
@pytest.fixture(name="test_file")
def fixture_test_file():
file = NamedTemporaryFile(dir=os.path.expanduser("~"), prefix="send2trash_test", delete=False)
file.close()
# Verify file was actually created
assert os.path.exists(file.name) is True
yield file.name
# Cleanup trash files on supported platforms
if sys.platform != "win32":
name = os.path.basename(file.name)
# Remove trash files if they exist
if os.path.exists(os.path.join(HOMETRASH, "files", name)):
os.remove(os.path.join(HOMETRASH, "files", name))
os.remove(os.path.join(HOMETRASH, "info", name + ".trashinfo"))
if os.path.exists(file.name):
os.remove(file.name)

View File

@@ -1,83 +1,62 @@
# encoding: utf-8 # encoding: utf-8
import pytest
import codecs import codecs
import os import os
import sys import sys
from os import path as op from os import path as op
from send2trash.compat import PY3
from send2trash import TrashPermissionError
try:
from configparser import ConfigParser
except ImportError:
# py2
from ConfigParser import ConfigParser # noqa: F401
from tempfile import mkdtemp, NamedTemporaryFile from tempfile import mkdtemp, NamedTemporaryFile
import shutil import shutil
import stat import stat
import uuid import uuid
from configparser import ConfigParser
import pytest
from send2trash import TrashPermissionError
if sys.platform != "win32": if sys.platform == "win32":
pytest.skip("Skipping non-windows tests", allow_module_level=True)
else:
import send2trash.plat_other import send2trash.plat_other
from send2trash.plat_other import send2trash as s2t from send2trash.plat_other import send2trash as s2t
from send2trash.plat_other import is_parent
INFO_SUFFIX = send2trash.plat_other.INFO_SUFFIX.decode() INFO_SUFFIX = send2trash.plat_other.INFO_SUFFIX.decode()
HOMETRASH = send2trash.plat_other.HOMETRASH HOMETRASH = send2trash.plat_other.HOMETRASH
else:
pytest.skip("Skipping non-windows tests", allow_module_level=True)
@pytest.fixture @pytest.fixture(name="test_files")
def testfile(): def fixture_test_files():
file = NamedTemporaryFile(dir=op.expanduser("~"), prefix="send2trash_test", delete=False)
file.close()
assert op.exists(file.name) is True
yield file
# Cleanup trash files on supported platforms
if sys.platform != "win32":
name = op.basename(file.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, "info", name + INFO_SUFFIX))
if op.exists(file.name):
os.remove(file.name)
@pytest.fixture
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), prefix=f"send2trash_test{index}",
delete=False, delete=False,
), ),
range(10), range(10),
) )
) )
[file.close() for file in files] for file in files:
assert all([op.exists(file.name) for file in files]) is True file.close()
assert all(op.exists(file.name) for file in files) is True
yield files yield files
filenames = [op.basename(file.name) for file in files] filenames = [op.basename(file.name) for file in files]
[os.remove(op.join(HOMETRASH, "files", filename)) for filename in filenames] for filename in filenames:
[os.remove(op.join(HOMETRASH, "info", filename + INFO_SUFFIX)) for filename in filenames] os.remove(op.join(HOMETRASH, "files", filename))
os.remove(op.join(HOMETRASH, "info", filename + INFO_SUFFIX))
def test_trash(testfile): def test_trash(test_file):
s2t(testfile.name) s2t(test_file)
assert op.exists(testfile.name) is False assert op.exists(test_file) is False
def test_multitrash(testfiles): def test_multitrash(test_files):
filenames = [file.name for file in testfiles] file_names = [file.name for file in test_files]
s2t(filenames) s2t(file_names)
assert any([op.exists(filename) for filename in filenames]) is False assert any(op.exists(filename) for filename in file_names) is False
def touch(path): def touch(path):
with open(path, "a"): with open(path, "a", encoding="utf-8"):
os.utime(path, None) os.utime(path, None)
@@ -87,9 +66,9 @@ def _filesys_enc():
return codecs.lookup(enc).name return codecs.lookup(enc).name
@pytest.fixture @pytest.fixture(name="gen_unicode_file")
def gen_unicode_file(): def fixture_gen_unicode_file():
name = u"send2trash_tést1" name = "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
@@ -117,14 +96,9 @@ def test_trash_unicode(gen_unicode_file):
class ExtVol: class ExtVol:
def __init__(self, path): def __init__(self, path):
self.trash_topdir = path self.trash_topdir = path
if PY3: self.trash_topdir_b = os.fsencode(self.trash_topdir)
self.trash_topdir_b = os.fsencode(self.trash_topdir)
else:
self.trash_topdir_b = self.trash_topdir
def s_getdev(path): def s_getdev(path):
from send2trash.plat_other import is_parent
st = os.lstat(path) st = os.lstat(path)
if is_parent(self.trash_topdir, path): if is_parent(self.trash_topdir, path):
return "dev" return "dev"
@@ -149,8 +123,8 @@ class ExtVol:
shutil.rmtree(self.trash_topdir) shutil.rmtree(self.trash_topdir)
@pytest.fixture @pytest.fixture(name="gen_ext_vol")
def gen_ext_vol(): def fixture_gen_ext_vol():
trash_topdir = mkdtemp(prefix="s2t") trash_topdir = mkdtemp(prefix="s2t")
volume = ExtVol(trash_topdir) volume = ExtVol(trash_topdir)
file_name = "test.txt" file_name = "test.txt"
@@ -167,23 +141,28 @@ def test_trash_topdir(gen_ext_vol):
s2t(gen_ext_vol[2]) s2t(gen_ext_vol[2])
assert op.exists(gen_ext_vol[2]) is False 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 ( if sys.platform == "darwin":
op.exists( # On macOS, we can only verify the file was removed from original location
op.join( pass
trash_dir, else:
str(os.getuid()), # Others platforms we can test
"info", assert op.exists(op.join(trash_dir, str(os.getuid()), "files", gen_ext_vol[1])) is True
gen_ext_vol[1] + INFO_SUFFIX, assert (
op.exists(
op.join(
trash_dir,
str(os.getuid()),
"info",
gen_ext_vol[1] + INFO_SUFFIX,
)
) )
is True
) )
is True
) cfg = ConfigParser()
# info relative path (if another test is added, with the same fileName/Path, cfg.read(op.join(trash_dir, str(os.getuid()), "info", gen_ext_vol[1] + INFO_SUFFIX))
# then it gets renamed etc.) assert (gen_ext_vol[1] == cfg.get("Trash Info", "Path", raw=True)) is True
cfg = ConfigParser()
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): def test_trash_topdir_fallback(gen_ext_vol):

View File

@@ -2,56 +2,98 @@
import os import os
import shutil import shutil
import sys import sys
import pytest
from os import path as op from os import path as op
import pytest
from send2trash import send2trash as s2t from send2trash import send2trash as s2t
# import the two versions as well as the "automatic" version s2t_modern = None
if sys.platform == "win32": s2t_legacy = None
if sys.platform != "win32":
pytest.skip("Skipping windows-only tests", allow_module_level=True)
else:
# import the two versions as well as the "automatic" version
from send2trash.win.modern import send2trash as s2t_modern from send2trash.win.modern import send2trash as s2t_modern
from send2trash.win.legacy import send2trash as s2t_legacy from send2trash.win.legacy import send2trash as s2t_legacy
else:
pytest.skip("Skipping windows-only tests", allow_module_level=True) if s2t_modern is None:
pytest.fail("Modern send2trash not available")
if s2t_legacy is None:
pytest.fail("Legacy send2trash not available")
def _create_tree(path): def _create_tree(path):
dirname = op.dirname(path) dir_name = op.dirname(path)
if not op.isdir(dirname): if not op.isdir(dir_name):
os.makedirs(dirname) os.makedirs(dir_name)
with open(path, "w") as writer: with open(path, "w", encoding="utf-8") as writer:
writer.write("send2trash test") writer.write("send2trash test")
@pytest.fixture @pytest.fixture(name="test_dir")
def testdir(tmp_path): def fixture_test_dir(tmp_path):
dirname = "\\\\?\\" + str(tmp_path) dir_name = "\\\\?\\" + str(tmp_path)
assert op.exists(dirname) is True assert op.exists(dir_name) is True
yield dirname yield dir_name
shutil.rmtree(dirname, ignore_errors=True) shutil.rmtree(dir_name, ignore_errors=True)
@pytest.fixture @pytest.fixture(name="test_file")
def testfile(testdir): def fixture_test_file(test_dir):
file = op.join(testdir, "testfile.txt") file = op.join(test_dir, "testfile.txt")
_create_tree(file) _create_tree(file)
assert op.exists(file) is True assert op.exists(file) is True
yield file yield file
# Note dir will cleanup the file # Note dir will cleanup the file
@pytest.fixture @pytest.fixture(name="test_files")
def testfiles(testdir): def fixture_test_files(test_dir):
files = [op.join(testdir, "testfile{}.txt".format(index)) for index in range(10)] files = [op.join(test_dir, f"testfile{index}.txt") for index in range(10)]
[_create_tree(file) for file in files] for file in files:
assert all([op.exists(file) for file in files]) is True _create_tree(file)
assert all(op.exists(file) for file in files) is True
yield files yield files
# Note dir will cleanup the files # Note dir will cleanup the files
def _trash_folder(dir, fcn): # Long path tests
fcn(dir) @pytest.fixture(name="long_dir")
assert op.exists(dir) is False def fixture_long_dir(tmp_path):
dir_name = "\\\\?\\" + str(tmp_path)
name = "A" * 100
yield op.join(dir_name, name, name, name)
try:
shutil.rmtree(dir_name, ignore_errors=True)
except TypeError:
pass
@pytest.fixture(name="long_file")
def fixture_long_file(long_dir):
name = "A" * 100
path = op.join(long_dir, name + "{}.txt")
file = path.format("")
_create_tree(file)
assert op.exists(file) is True
yield file
@pytest.fixture(name="long_files")
def fixture_long_files(long_dir):
name = "A" * 100
path = op.join(long_dir, name + "{}.txt")
files = [path.format(index) for index in range(10)]
for file in files:
_create_tree(file)
assert all(op.exists(file) for file in files) is True
yield files
def _trash_folder(folder, fcn):
fcn(folder)
assert op.exists(folder) is False
def _trash_file(file, fcn): def _trash_file(file, fcn):
@@ -61,160 +103,113 @@ def _trash_file(file, fcn):
def _trash_multifile(files, fcn): def _trash_multifile(files, fcn):
fcn(files) fcn(files)
assert any([op.exists(file) for file in files]) is False assert any(op.exists(file) for file in files) is False
def _file_not_found(dir, fcn): def _file_not_found(folder, fcn):
file = op.join(dir, "otherfile.txt") file = op.join(folder, "otherfile.txt")
pytest.raises(OSError, fcn, file) pytest.raises(OSError, fcn, file)
def _multi_byte_unicode(dir, fcn): def _multi_byte_unicode(folder, fcn):
single_file = op.join(dir, "😇.txt") single_file = op.join(folder, "😇.txt")
_create_tree(single_file) _create_tree(single_file)
assert op.exists(single_file) is True assert op.exists(single_file) is True
fcn(single_file) fcn(single_file)
assert op.exists(single_file) is False assert op.exists(single_file) is False
files = [op.join(dir, "😇{}.txt".format(index)) for index in range(10)] files = [op.join(folder, f"😇{index}.txt") for index in range(10)]
[_create_tree(file) for file in files] for file in files:
assert all([op.exists(file) for file in files]) is True _create_tree(file)
assert all(op.exists(file) for file in files) is True
fcn(files) fcn(files)
assert any([op.exists(file) for file in files]) is False assert any(op.exists(file) for file in files) is False
def test_trash_folder(testdir): def test_trash_folder(test_dir):
_trash_folder(testdir, s2t) _trash_folder(test_dir, s2t)
def test_trash_file(testfile): def test_trash_file(test_file):
_trash_file(testfile, s2t) _trash_file(test_file, s2t)
def test_trash_multifile(testfiles): def test_trash_multifile(test_files):
_trash_multifile(testfiles, s2t) _trash_multifile(test_files, s2t)
def test_file_not_found(testdir): def test_file_not_found(test_dir):
_file_not_found(testdir, s2t) _file_not_found(test_dir, s2t)
def test_trash_folder_modern(testdir): def test_trash_folder_modern(test_dir):
_trash_folder(testdir, s2t_modern) _trash_folder(test_dir, s2t_modern)
def test_trash_file_modern(testfile): def test_trash_file_modern(test_file):
_trash_file(testfile, s2t_modern) _trash_file(test_file, s2t_modern)
def test_trash_multifile_modern(testfiles): def test_trash_multifile_modern(test_files):
_trash_multifile(testfiles, s2t_modern) _trash_multifile(test_files, s2t_modern)
def test_file_not_found_modern(testdir): def test_file_not_found_modern(test_dir):
_file_not_found(testdir, s2t_modern) _file_not_found(test_dir, s2t_modern)
def test_multi_byte_unicode_modern(testdir): def test_multi_byte_unicode_modern(test_dir):
_multi_byte_unicode(testdir, s2t_modern) _multi_byte_unicode(test_dir, s2t_modern)
def test_trash_folder_legacy(testdir):
_trash_folder(testdir, s2t_legacy)
def test_trash_file_legacy(testfile):
_trash_file(testfile, s2t_legacy)
def test_trash_multifile_legacy(testfiles):
_trash_multifile(testfiles, s2t_legacy)
def test_file_not_found_legacy(testdir):
_file_not_found(testdir, s2t_legacy)
def test_multi_byte_unicode_legacy(testdir):
_multi_byte_unicode(testdir, s2t_legacy)
# Long path tests
@pytest.fixture
def longdir(tmp_path):
dirname = "\\\\?\\" + str(tmp_path)
name = "A" * 100
yield op.join(dirname, name, name, name)
try:
shutil.rmtree(dirname, ignore_errors=True)
except TypeError:
pass
@pytest.fixture
def longfile(longdir):
name = "A" * 100
path = op.join(longdir, name + "{}.txt")
file = path.format("")
_create_tree(file)
assert op.exists(file) is True
yield file
@pytest.fixture
def longfiles(longdir):
name = "A" * 100
path = op.join(longdir, name + "{}.txt")
files = [path.format(index) for index in range(10)]
[_create_tree(file) for file in files]
assert all([op.exists(file) for file in files]) is True
yield files
# NOTE: both legacy and modern test "pass" on windows, however sometimes with the same path # NOTE: both legacy and modern test "pass" on windows, however sometimes with the same path
# they do not actually recycle files but delete them. Noticed this when testing with the # they do not actually recycle files but delete them. Noticed this when testing with the
# recycle bin open, noticed later tests actually worked, modern version can actually detect # recycle bin open, noticed later tests actually worked, modern version can actually detect
# when this happens but not stop it at this moment, and we need a way to verify it when testing. # when this happens but not stop it at this moment, and we need a way to verify it when testing.
def test_trash_long_file_modern(longfile): def test_trash_long_file_modern(long_file):
_trash_file(longfile, s2t_modern) _trash_file(long_file, s2t_modern)
def test_trash_long_multifile_modern(longfiles): def test_trash_long_multifile_modern(long_files):
_trash_multifile(longfiles, s2t_modern) _trash_multifile(long_files, s2t_modern)
# @pytest.skipif(
# op.splitdrive(os.getcwd())[0] != op.splitdrive(gettempdir())[0],
# "Cannot trash long path from other drive",
# )
# def test_trash_long_folder_modern(self):
# self._trash_folder(s2t_modern)
def test_trash_long_file_legacy(longfile):
_trash_file(longfile, s2t_legacy)
def test_trash_long_multifile_legacy(longfiles):
_trash_multifile(longfiles, s2t_legacy)
# @pytest.skipif(
# op.splitdrive(os.getcwd())[0] != op.splitdrive(gettempdir())[0],
# "Cannot trash long path from other drive",
# )
# 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(): def test_trash_nothing_modern():
try: try:
s2t_modern([]) s2t_modern([])
except Exception as ex: except Exception as ex:
assert False, "Exception thrown when trashing nothing: {}".format(ex) assert False, f"Exception thrown when trashing nothing: {ex}"
def test_trash_folder_legacy(test_dir):
_trash_folder(test_dir, s2t_legacy)
def test_trash_file_legacy(test_file):
_trash_file(test_file, s2t_legacy)
def test_trash_multifile_legacy(test_files):
_trash_multifile(test_files, s2t_legacy)
def test_file_not_found_legacy(test_dir):
_file_not_found(test_dir, s2t_legacy)
def test_multi_byte_unicode_legacy(test_dir):
_multi_byte_unicode(test_dir, s2t_legacy)
def test_trash_long_file_legacy(long_file):
_trash_file(long_file, s2t_legacy)
def test_trash_long_multifile_legacy(long_files):
_trash_multifile(long_files, s2t_legacy)
def test_trash_nothing_legacy():
try:
s2t_legacy([])
except Exception as ex:
assert False, f"Exception thrown when trashing nothing: {ex}"

View File

@@ -1,41 +1,16 @@
# encoding: utf-8 # encoding: utf-8
import os
import sys
import pytest
from tempfile import NamedTemporaryFile
from os import path as op from os import path as op
import pytest
from send2trash.__main__ import main as trash_main from send2trash.__main__ import main as trash_main
# Only import HOMETRASH on supported platforms
if sys.platform != "win32": def test_trash(test_file):
from send2trash.plat_other import HOMETRASH trash_main(["-v", test_file])
assert op.exists(test_file) is False
@pytest.fixture def test_no_args(test_file):
def file():
file = NamedTemporaryFile(dir=op.expanduser("~"), prefix="send2trash_test", delete=False)
file.close()
# Verify file was actually created
assert op.exists(file.name) is True
yield file.name
# Cleanup trash files on supported platforms
if sys.platform != "win32":
name = op.basename(file.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, "info", name + ".trashinfo"))
if op.exists(file.name):
os.remove(file.name)
def test_trash(file):
trash_main(["-v", file])
assert op.exists(file) is False
def test_no_args(file):
pytest.raises(SystemExit, trash_main, []) pytest.raises(SystemExit, trash_main, [])
pytest.raises(SystemExit, trash_main, ["-v"]) pytest.raises(SystemExit, trash_main, ["-v"])
assert op.exists(file) is True assert op.exists(test_file) is True

View File

@@ -1,5 +1,5 @@
[tox] [tox]
envlist = py{27,34,35,36,37,38,39,310} envlist = py{38,39,310,311,312,313,314}
skip_missing_interpreters = True skip_missing_interpreters = True
isolated_build = True isolated_build = True
@@ -13,11 +13,6 @@ commands =
flake8 flake8
pytest pytest
[testenv:py27]
deps =
configparser
{[testenv]deps}
[flake8] [flake8]
exclude = .tox,env,build exclude = .tox,env,build
max-line-length = 120 max-line-length = 120