1
0
mirror of https://github.com/arsenetar/send2trash.git synced 2026-01-22 14:41:40 +00:00

Merge pull request #103 from arsenetar/as/ci-lang-updates

feat: CI, Lanugage, lint, and misc cleanups
This commit is contained in:
2025-12-30 22:06:40 -06:00
committed by GitHub
15 changed files with 267 additions and 341 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

@@ -8,9 +8,9 @@ jobs:
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Python 3.x - name: Set up Python 3.x
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with: with:
python-version: 3.x python-version: 3.x
- name: Install dependencies - name: Install dependencies
@@ -26,6 +26,8 @@ jobs:
strategy: strategy:
matrix: matrix:
include: include:
- os: ubuntu-latest
python-version: 3.14
- os: ubuntu-latest - os: ubuntu-latest
python-version: 3.13 python-version: 3.13
- os: ubuntu-latest - os: ubuntu-latest
@@ -38,19 +40,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: macos-latest - os: macos-latest
# python-version: 3.13 python-version: 3.14
# - os: macos-latest - os: macos-latest
# python-version: 3.8 python-version: 3.9
- os: windows-latest - os: windows-latest
python-version: 3.13 python-version: 3.14
- os: windows-latest - os: windows-latest
python-version: 3.8 python-version: 3.8
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - 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@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 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,11 @@
Changes Changes
======= =======
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 Version 1.8.3 -- 2024/04/06
--------------------------- ---------------------------

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
------------------------------- -------------------------------
@@ -58,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

@@ -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,36 +17,15 @@
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
try:
from urllib.parse import quote from urllib.parse import quote
except ImportError:
# Python 2
from urllib import quote
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"
@@ -54,7 +33,7 @@ INFO_SUFFIX = b".trashinfo"
# Default of ~/.local/share [3] # Default of ~/.local/share [3]
XDG_DATA_HOME = op.expanduser(os.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"
@@ -64,10 +43,10 @@ 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, str): if isinstance(path, str):
path = fsencode(path) path = os.fsencode(path)
parent = op.realpath(parent) parent = op.realpath(parent)
if isinstance(parent, str): if isinstance(parent, str):
parent = fsencode(parent) parent = os.fsencode(parent)
return path.startswith(parent) return path.startswith(parent)
@@ -89,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" " + str(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(fsdecode(src), fsdecode(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):
@@ -138,7 +117,7 @@ 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, str(uid).encode("ascii")) trash_dir = op.join(trash_dir, str(uid).encode("ascii"))
@@ -157,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,18 +157,18 @@ def send2trash(paths):
paths = preprocess_paths(paths) paths = preprocess_paths(paths)
for path in paths: for path in paths:
if isinstance(path, str): 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
@@ -205,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,6 +5,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
import os
import collections.abc import collections.abc
@@ -14,5 +15,4 @@ def preprocess_paths(paths):
elif not isinstance(paths, list): 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 = [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,9 +6,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import os.path as op import os.path as op
from send2trash.util import preprocess_paths
from ctypes import ( from ctypes import (
windll, windll,
Structure, Structure,
@@ -21,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

View File

@@ -6,11 +6,11 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import os.path as op import os.path as op
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
@@ -59,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,6 +1,6 @@
[metadata] [metadata]
name = Send2Trash name = Send2Trash
version = 2.0.0-dev version = 2.0.0
url = https://github.com/arsenetar/send2trash url = https://github.com/arsenetar/send2trash
project_urls = project_urls =
Bug Reports = https://github.com/arsenetar/send2trash/issues Bug Reports = https://github.com/arsenetar/send2trash/issues
@@ -25,6 +25,7 @@ classifiers =
Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.11
Programming Language :: Python :: 3.12 Programming Language :: Python :: 3.12
Programming Language :: Python :: 3.13 Programming Language :: Python :: 3.13
Programming Language :: Python :: 3.14
Topic :: Desktop Environment :: File Managers Topic :: Desktop Environment :: File Managers
[options] [options]

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,82 +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 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)
@@ -86,8 +66,8 @@ 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 = "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)
@@ -119,8 +99,6 @@ class ExtVol:
self.trash_topdir_b = os.fsencode(self.trash_topdir) self.trash_topdir_b = os.fsencode(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"
@@ -145,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"

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
s2t_modern = None
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 # import the two versions as well as the "automatic" version
if sys.platform == "win32":
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
fcn(files)
assert any([op.exists(file) for file in files]) is False
def test_trash_folder(testdir):
_trash_folder(testdir, s2t)
def test_trash_file(testfile):
_trash_file(testfile, s2t)
def test_trash_multifile(testfiles):
_trash_multifile(testfiles, s2t)
def test_file_not_found(testdir):
_file_not_found(testdir, s2t)
def test_trash_folder_modern(testdir):
_trash_folder(testdir, s2t_modern)
def test_trash_file_modern(testfile):
_trash_file(testfile, s2t_modern)
def test_trash_multifile_modern(testfiles):
_trash_multifile(testfiles, s2t_modern)
def test_file_not_found_modern(testdir):
_file_not_found(testdir, s2t_modern)
def test_multi_byte_unicode_modern(testdir):
_multi_byte_unicode(testdir, s2t_modern)
def test_trash_folder_legacy(testdir):
_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) _create_tree(file)
assert op.exists(file) is True assert all(op.exists(file) for file in files) is True
yield file fcn(files)
assert any(op.exists(file) for file in files) is False
@pytest.fixture def test_trash_folder(test_dir):
def longfiles(longdir): _trash_folder(test_dir, s2t)
name = "A" * 100
path = op.join(longdir, name + "{}.txt")
files = [path.format(index) for index in range(10)] def test_trash_file(test_file):
[_create_tree(file) for file in files] _trash_file(test_file, s2t)
assert all([op.exists(file) for file in files]) is True
yield files
def test_trash_multifile(test_files):
_trash_multifile(test_files, s2t)
def test_file_not_found(test_dir):
_file_not_found(test_dir, s2t)
def test_trash_folder_modern(test_dir):
_trash_folder(test_dir, s2t_modern)
def test_trash_file_modern(test_file):
_trash_file(test_file, s2t_modern)
def test_trash_multifile_modern(test_files):
_trash_multifile(test_files, s2t_modern)
def test_file_not_found_modern(test_dir):
_file_not_found(test_dir, s2t_modern)
def test_multi_byte_unicode_modern(test_dir):
_multi_byte_unicode(test_dir, s2t_modern)
# 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{38,39,310,311,312,313} envlist = py{38,39,310,311,312,313,314}
skip_missing_interpreters = True skip_missing_interpreters = True
isolated_build = True isolated_build = True