Compare commits

...

80 Commits

Author SHA1 Message Date
Andrew Senetar 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
Andrew Senetar 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
Andrew Senetar 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
Andrew Senetar 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
Andrew Senetar 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
Andrew Senetar 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
Andrew Senetar 0244f53e2e
fix(build): Fix syntax in setup.cfg, add python 3.11 2023-04-27 00:28:59 -05:00
Andrew Senetar 1625d56345
Update version & changelog for 1.8.2 2023-04-27 00:22:55 -05:00
Andrew Senetar 63e770d29e
Merge pull request #78 from arsenetar/as/fix-ci
fix(ci): Update to latest actions, fix python versions
2023-04-27 00:03:55 -05:00
Andrew Senetar bfd3e08661
fix(ci): Update to latest actions, fix python versions
- Update to latest actions
- Change OS for older python 3.6, 3.5 to run
2023-04-26 23:54:52 -05:00
Andrew Senetar 490fe02245
Merge pull request #73 from sobolevn/patch-1
TravisCI is not used anymore
2023-04-26 23:54:24 -05:00
Andrew Senetar d6d904c774
Merge pull request #77 from BoboTiG/patch-1
win/legacy: tiny logic simplification
2023-04-26 23:36:39 -05:00
Mickaël Schoentgen 0a36688a4b
win/legacy: tiny logic simplification 2023-04-02 17:08:25 +02:00
Nikita Sobolev 955f8091da
TravisCI is not used anymore 2022-10-11 23:57:46 +03:00
Andrew Senetar 0ef9b3294a
fix(tests): Correct windows tests to run on python <3.6 2022-07-26 22:51:16 -05:00
Andrew Senetar 4b9bc4bc31
fix(win): Prevent exception on empty list
Add check for when an empty list remains after preprocessing and do
not continue for both legacy and modern windows implementations.

Fix #71
2022-07-26 22:42:30 -05:00
Andrew Senetar be402728fb
Update setup.cfg for changes in d37197c, fix #68. 2022-06-01 02:06:14 -05:00
Andrew Senetar 448224954b
Replace relative imports with absolute 2022-06-01 00:22:46 -05:00
Andrew Senetar d37197c4f7
Move mac/win to subpackages & fix #64
- Move macOS and Windows implementations to sub packagese to improve organization
- Fix #64 in legacy windows implementation by mapping results to standard error codes
2022-04-30 19:52:09 -05:00
Andrew Senetar 2a88b82104
Fix test_plat_other from previous change 2021-08-24 01:21:12 -05:00
Andrew Senetar 18e51c0b5a
Minor cleanup in plat_other
- Add OSError code values
- Use INFO_SUFFIX constant in tests
- Remove old PathLike conversions
2021-08-24 01:00:02 -05:00
Andrew Senetar 7686647389
Fix flake8 error 2021-08-21 16:04:05 -05:00
Andrew Senetar 696aed558b
Change method for test symlink path generation 2021-08-21 16:00:50 -05:00
Andrew Senetar 007d84361a
Fix items missed in test_plat_other in last commit 2021-08-21 15:22:59 -05:00
Andrew Senetar 78a536abba
Minor code quality updates 2021-08-21 15:19:32 -05:00
Andrew Senetar 2aa834be94
Update version & changelog for 1.8.1b0 2021-08-20 22:42:04 -05:00
Andrew Senetar 5e4517aa53
Add fallback to HOMETRASH on plat_other
In the case where os.path.ismount() does not detect a mount and os.rename errors
fallback to HOMETRASH.  This covers several situations where continuing with
the identified trash location is incorrect due to complex mounts.

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

  Traceback (most recent call last):
    File "check.py", line 6, in <module>
      send2trash(str(file))
    File "\...\plat_win_legacy.py", line 79, in send2trash
      paths = [get_short_path_name(path) for path in paths]
    File "\...\plat_win_legacy.py", line 79, in <listcomp>
      paths = [get_short_path_name(path) for path in paths]
    File "\...\plat_win_legacy.py", line 62, in get_short_path_name
      raise WindowsError(err_no, FormatError(err_no), long_name[4:])
  OSError: [Errno 123] La syntaxe du nom de fichier, de répertoire ou de volume est incorrecte.: '\\\\SERVER\\folder\\file.txt'
2021-05-26 17:22:26 +02:00
Andrew Senetar 23ce7b8c16
Bump version 2021-05-14 21:44:21 -05:00
Andrew Senetar 9b0d5796c1
Change conditional for macos pyobjc usage
macOS 11.x will occasionally identify as 10.16, since there was no real
reason to prevent on all supported platforms allow.
2021-05-14 21:40:16 -05:00
Andrew Senetar c8bcaea1e8
Update version and changelog for release 2021-04-20 17:35:59 -05:00
Andrew Senetar 530e9b4bc6
Add initial pyobjc version for macOS
This is to help with issue #51.  Will not help in the case of python 2 or
older python 3 version < 3.6.
2021-04-13 22:36:10 -05:00
Andrew Senetar 10c7693d11
Minor fixes to tests 2021-03-17 21:51:51 -05:00
Andrew Senetar 356509120b
Add some checks to catch test failure
Really just checking that the setup is able to create test files so it
is known they were there then removed.
Windows tests really need verification of
recycle, which is not present.
2021-03-17 20:52:16 -05:00
Andrew Senetar f9fcdb8d8c
Fix legacy windows platform for multibyte unicode
- Add handling to create correctly sized buffer even with multibyte
characters as len() in python does not line up with what
create_unicode_buffer() needs for length.
- Add test for single and multiple files
2021-03-10 21:41:30 -06:00
Andrew Senetar af0c1ba704
More test fixes
- Fix travis windows env
- Fix exception type in test_plat_win
- Finish converting tests in test_plat_other, fix exception type
2021-03-10 18:57:35 -06:00
Andrew Senetar 37be84d46e
Update .travis.yml 2021-03-02 22:48:48 -06:00
Andrew Senetar 9f76fbf036
Updates for tests on windows
- Other platform tests WIP
- Windows long directory tests WIP
2021-03-02 19:33:44 -06:00
Andrew Senetar a324923ffa
Add IFileOperationProgressSink
- Add sink to allow detection of times when the file would be deleted
- Currently can detect, but not stop operations, more work needed
2021-03-02 19:23:43 -06:00
Andrew Senetar dbdcce8b04
First batch of updates to unit tests
- Remove content from __init__.py
- Change test_script_main to use pytest
- Update test_script_main to run on windows as well as linux
2021-03-02 00:26:29 -06:00
Andrew Senetar 054d56c564
Update Tox and Travis configurations 2021-03-02 00:24:59 -06:00
Andrew Senetar 33ed07811b
Cleanup flake8 issues 2021-03-01 23:44:03 -06:00
Andrew Senetar 5d3835735e
Merge pull request #55 from juliangilbey/fix-win-test
Only import Windows-specific modules when on Windows
2021-01-28 23:53:24 -06:00
Julian Gilbey 741c7ad51f Only import Windows-specific modules when on Windows 2021-01-29 05:42:11 +00:00
Andrew Senetar 2eb3242cd9
Merge pull request #54 from asellappen/master
Adding power support for this package to support arch independent
2021-01-21 19:11:15 -06:00
Andrew Senetar 60bcb2c834
Merge pull request #47 from pracedru/master
Update plat_other.py
2021-01-21 19:00:46 -06:00
Andrew Senetar c411f4eae4
Merge branch 'master' into master 2021-01-21 19:00:33 -06:00
Andrew Senetar f64c69f905
Merge branch 'master' into master 2021-01-21 18:57:39 -06:00
Andrew Senetar 00dfe77e40
Add console_script entry point, close #50 2021-01-12 18:22:23 -06:00
Andrew Senetar 16a7115ff1
Merge pull request #52 from BoboTiG/impr-expand-ci
Expand supported Python versions
2021-01-12 16:58:09 -06:00
Andrew Senetar ec73b44c43
Merge pull request #53 from BoboTiG/fix-resource-warning
Fix ResourceWarning: unclosed file in setup.py
2021-01-12 16:53:59 -06:00
Arumugam f62b4f1ffd Adding power support for this package to support arch indepentant 2020-12-15 06:08:33 +00:00
Mickaël Schoentgen 38ae2b63d2 Expand supported Python versions 2020-12-01 09:16:20 +01:00
Mickaël Schoentgen cd8d9fb95e Fix ResourceWarning: unclosed file in setup.py
Also prevent potential identical warning in `plat_other.py`.
2020-12-01 08:45:46 +01:00
Magnus Møller Jørgensen 20bbab0b4c
Update plat_other.py
The trash info file needs to exist before the file is moved into the trash folder. 
This is to conform to the events based detection of trashed files in gnome and other file managers.
2020-07-23 03:57:15 +02:00
28 changed files with 1033 additions and 609 deletions

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

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

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

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

View File

@ -1,28 +0,0 @@
language: python
matrix:
include:
- os: windows
language: sh
python: 3
# Perform the manual steps on windows to install python3
before_install:
- choco install python3 --params "/InstallDir:C:\Python"
- export PATH="/c/Python:/c/Python/Scripts:$PATH"
- python -m pip install --upgrade pip
- python -m pip install pywin32
before_script:
- export TOXENV=py3-win
- python: "2.7"
- python: "3.4"
- python: "3.5"
- python: "3.6"
# Obtain Python 3.7 from xenial as per https://github.com/travis-ci/travis-ci/issues/9815
- python: "3.7"
dist: xenial
install:
- pip install tox
before_script:
- export TOXENV=$(echo py$TRAVIS_PYTHON_VERSION | tr -d .)
script:
- tox

View File

@ -1,6 +1,50 @@
Changes
=======
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
---------------------------
* win/legacy: tiny logic simplification by @BoboTiG in https://github.com/arsenetar/send2trash/pull/77
* TravisCI is not used anymore by @sobolevn in https://github.com/arsenetar/send2trash/pull/73
* fix(ci): Update to latest actions, fix python versions by @arsenetar in https://github.com/arsenetar/send2trash/pull/78
Version 1.8.1b0 -- 2021/09/20
-----------------------------
* Add fallback to HOMETRASH when cross device errors happen in plat_other (#26, #41, #63)
Version 1.8.0 -- 2021/08/08
---------------------------
* Add compatibility with pathlib paths (#49)
* Fix thread compatibility of modern windows implementation (#59)
* Fix handling of UNC names in legacy windows implementation (#57)
Version 1.7.1 -- 2021/06/21
---------------------------
* Release stable version with changes from last 3 releases
* Fix handling of UNC names (#57)
Version 1.7.0a1 -- 2021/05/14
-----------------------------
* Changed conditional for when to try to use pyobjc version (#51)
Version 1.7.0a0 -- 2021/04/20
-----------------------------
* Add console_script entry point (#50)
* Increased python CI versions (#52, #54)
* Fix minor issue in setup.py (#53)
* Fix issue with windows tests importing modules on non-windows (#55)
* Unit test cleanups, rewrites, and flake8 cleanups
* Windows: Fix legacy windows platform for multi-byte unicode and add tests
* macOS: Add alternative pyobjc version to potentially improve compatibility (#51)
Version 1.6.0b1 -- 2020/06/18
-----------------------------

View File

@ -3,11 +3,11 @@ Send2Trash -- Send files to trash on all platforms
==================================================
Send2Trash is a small package that sends files to the Trash (or Recycle Bin) *natively* and on
*all platforms*. On OS X, it uses native ``FSMoveObjectToTrashSync`` Cocoa calls. On Windows, it
uses native ``IFileOperation`` call if on Vista or newer and pywin32 is installed or falls back
to ``SHFileOperation`` calls. On other platforms, if `PyGObject`_ and `GIO`_ are available, it
will use this. Otherwise, it will fallback to its own implementation of the `trash specifications
from freedesktop.org`_.
*all platforms*. On OS X, it uses native ``FSMoveObjectToTrashSync`` Cocoa calls or can use pyobjc
with NSFileManager. On Windows, it uses native ``IFileOperation`` call if on Vista or newer and
pywin32 is installed or falls back to ``SHFileOperation`` calls. On other platforms, if `PyGObject`_
and `GIO`_ are available, it will use this. Otherwise, it will fallback to its own implementation of
the `trash specifications from freedesktop.org`_.
``ctypes`` is used to access native libraries, so no compilation is necessary.
@ -22,9 +22,13 @@ issues and fixes would be most appreciated.
Installation
------------
You can download it with pip::
You can download it with pip:
pip install Send2Trash
python -m pip install -U send2trash
To install with pywin32 or pyobjc required specify the extra `nativeLib`:
python -m pip install -U send2trash[nativeLib]
or you can download the source from http://github.com/arsenetar/send2trash and install it with::

6
pyproject.toml Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,20 @@
# 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
from platform import mac_ver
from sys import version_info
# NOTE: version of pyobjc only supports python >= 3.6 and 10.9+
macos_ver = tuple(int(part) for part in mac_ver()[0].split("."))
if version_info >= (3, 6) and macos_ver >= (10, 9):
try:
from send2trash.mac.modern import send2trash
except ImportError:
# Try to fall back to ctypes version, although likely problematic still
from send2trash.mac.legacy import send2trash
else:
# Just use the old version otherwise
from send2trash.mac.legacy import send2trash # noqa: F401

View File

@ -9,7 +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 send2trash.compat import binary_type
from send2trash.util import preprocess_paths
Foundation = cdll.LoadLibrary(find_library("Foundation"))
CoreServices = cdll.LoadLibrary(find_library("CoreServices"))
@ -40,12 +41,8 @@ def check_op_result(op_result):
def send2trash(paths):
if not isinstance(paths, list):
paths = [paths]
paths = [
path.encode("utf-8") if not isinstance(path, binary_type) else path
for path in paths
]
paths = preprocess_paths(paths)
paths = [path.encode("utf-8") if not isinstance(path, binary_type) else path for path in paths]
for path in paths:
fp = FSRef()
opts = kFSPathMakeRefDoNotFollowLeafSymlink

26
send2trash/mac/modern.py Normal file
View File

@ -0,0 +1,26 @@
# 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
from Foundation import NSFileManager, NSURL
from send2trash.compat import text_type
from send2trash.util import preprocess_paths
def check_op_result(op_result):
# First value will be false on failure
if not op_result[0]:
# Error is in third value, localized failure reason matchs ctypes version
raise OSError(op_result[2].localizedFailureReason())
def send2trash(paths):
paths = preprocess_paths(paths)
paths = [path.decode("utf-8") if not isinstance(path, text_type) else path for path in paths]
for path in paths:
file_url = NSURL.fileURLWithPath_(path)
fm = NSFileManager.defaultManager()
op_result = fm.trashItemAtURL_resultingItemURL_error_(file_url, None, None)
check_op_result(op_result)

View File

@ -5,12 +5,12 @@
# http://www.hardcoded.net/licenses/bsd_license
from gi.repository import GObject, Gio
from .exceptions import TrashPermissionError
from send2trash.exceptions import TrashPermissionError
from send2trash.util import preprocess_paths
def send2trash(paths):
if not isinstance(paths, list):
paths = [paths]
paths = preprocess_paths(paths)
for path in paths:
try:
f = Gio.File.new_for_path(path)

View File

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

View File

@ -1,105 +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
from __future__ import unicode_literals
import os.path as op
from .compat import text_type
from ctypes import (
windll,
Structure,
byref,
c_uint,
create_unicode_buffer,
addressof,
GetLastError,
FormatError,
)
from ctypes.wintypes import HWND, UINT, LPCWSTR, BOOL
kernel32 = windll.kernel32
GetShortPathNameW = kernel32.GetShortPathNameW
shell32 = windll.shell32
SHFileOperationW = shell32.SHFileOperationW
class SHFILEOPSTRUCTW(Structure):
_fields_ = [
("hwnd", HWND),
("wFunc", UINT),
("pFrom", LPCWSTR),
("pTo", LPCWSTR),
("fFlags", c_uint),
("fAnyOperationsAborted", BOOL),
("hNameMappings", c_uint),
("lpszProgressTitle", LPCWSTR),
]
FO_MOVE = 1
FO_COPY = 2
FO_DELETE = 3
FO_RENAME = 4
FOF_MULTIDESTFILES = 1
FOF_SILENT = 4
FOF_NOCONFIRMATION = 16
FOF_ALLOWUNDO = 64
FOF_NOERRORUI = 1024
def get_short_path_name(long_name):
if not long_name.startswith("\\\\?\\"):
long_name = "\\\\?\\" + long_name
buf_size = GetShortPathNameW(long_name, None, 0)
# FIX: https://github.com/hsoft/send2trash/issues/31
# If buffer size is zero, an error has occurred.
if not buf_size:
err_no = GetLastError()
raise WindowsError(err_no, FormatError(err_no), long_name[4:])
output = create_unicode_buffer(buf_size)
GetShortPathNameW(long_name, output, buf_size)
return output.value[4:] # Remove '\\?\' for SHFileOperationW
def send2trash(paths):
if not isinstance(paths, list):
paths = [paths]
# convert data type
paths = [
text_type(path, "mbcs") if not isinstance(path, text_type) else path
for path in paths
]
# convert to full paths
paths = [op.abspath(path) if not op.isabs(path) else path for path in paths]
# get short path to handle path length issues
paths = [get_short_path_name(path) for path in paths]
# convert to a single string of null terminated paths
paths = "\0".join(paths)
fileop = SHFILEOPSTRUCTW()
fileop.hwnd = 0
fileop.wFunc = FO_DELETE
# FIX: https://github.com/hsoft/send2trash/issues/17
# Starting in python 3.6.3 it is no longer possible to use:
# LPCWSTR(path + '\0') directly as embedded null characters are no longer
# allowed in strings
# Workaround
# - create buffer of c_wchar[] (LPCWSTR is based on this type)
# - buffer is two c_wchar characters longer (double null terminator)
# - cast the address of the buffer to a LPCWSTR
# NOTE: based on how python allocates memory for these types they should
# always be zero, if this is ever not true we can go back to explicitly
# setting the last two characters to null using buffer[index] = '\0'.
buffer = create_unicode_buffer(paths, len(paths) + 2)
fileop.pFrom = LPCWSTR(addressof(buffer))
fileop.pTo = None
fileop.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT
fileop.fAnyOperationsAborted = 0
fileop.hNameMappings = 0
fileop.lpszProgressTitle = None
result = SHFileOperationW(byref(fileop))
if result:
raise WindowsError(result, FormatError(result), paths)

18
send2trash/util.py Normal file
View File

@ -0,0 +1,18 @@
# encoding: utf-8
# Copyright 2017 Virgil Dupras
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
from send2trash.compat import text_type, binary_type, iterable_type
def preprocess_paths(paths):
if isinstance(paths, iterable_type) and not isinstance(paths, (text_type, binary_type)):
paths = list(paths)
elif not isinstance(paths, list):
paths = [paths]
# Convert items such as pathlib paths to strings
paths = [path.__fspath__() if hasattr(path, "__fspath__") else path for path in paths]
return paths

View File

@ -0,0 +1,46 @@
# Sample implementation of IFileOperationProgressSink that just prints
# some basic info
import pythoncom
from win32com.shell import shell, shellcon
from win32com.server.policy import DesignatedWrapPolicy
class FileOperationProgressSink(DesignatedWrapPolicy):
_com_interfaces_ = [shell.IID_IFileOperationProgressSink]
_public_methods_ = [
"StartOperations",
"FinishOperations",
"PreRenameItem",
"PostRenameItem",
"PreMoveItem",
"PostMoveItem",
"PreCopyItem",
"PostCopyItem",
"PreDeleteItem",
"PostDeleteItem",
"PreNewItem",
"PostNewItem",
"UpdateProgress",
"ResetTimer",
"PauseTimer",
"ResumeTimer",
]
def __init__(self):
self._wrap_(self)
self.newItem = None
def PreDeleteItem(self, flags, item):
# Can detect cases where to stop via flags and condition below, however the operation
# does not actual stop, we can resort to raising an exception as that does stop things
# but that may need some additional considerations before implementing.
return 0 if flags & shellcon.TSF_DELETE_RECYCLE_IF_POSSIBLE else 0x80004005 # S_OK, or E_FAIL
def PostDeleteItem(self, flags, item, hr_delete, newly_created):
if newly_created:
self.newItem = newly_created.GetDisplayName(shellcon.SHGDN_FORPARSING)
def create_sink():
return pythoncom.WrapObject(FileOperationProgressSink(), shell.IID_IFileOperationProgressSink)

View File

@ -11,10 +11,10 @@ from platform import version
if int(version().split(".", 1)[0]) >= 6:
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

183
send2trash/win/legacy.py Normal file
View File

@ -0,0 +1,183 @@
# 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
from __future__ import unicode_literals
import os.path as op
from send2trash.compat import text_type
from send2trash.util import preprocess_paths
from ctypes import (
windll,
Structure,
byref,
c_uint,
create_unicode_buffer,
addressof,
GetLastError,
FormatError,
)
from ctypes.wintypes import HWND, UINT, LPCWSTR, BOOL
kernel32 = windll.kernel32
GetShortPathNameW = kernel32.GetShortPathNameW
shell32 = windll.shell32
SHFileOperationW = shell32.SHFileOperationW
class SHFILEOPSTRUCTW(Structure):
_fields_ = [
("hwnd", HWND),
("wFunc", UINT),
("pFrom", LPCWSTR),
("pTo", LPCWSTR),
("fFlags", c_uint),
("fAnyOperationsAborted", BOOL),
("hNameMappings", c_uint),
("lpszProgressTitle", LPCWSTR),
]
FO_MOVE = 1
FO_COPY = 2
FO_DELETE = 3
FO_RENAME = 4
FOF_MULTIDESTFILES = 1
FOF_SILENT = 4
FOF_NOCONFIRMATION = 16
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)
are handled.
Return a tuple of the long-path prefix and the prefixed path.
"""
prefix, long_path = "\\\\?\\", path
if not path.startswith(prefix):
if path.startswith("\\\\"):
# Likely a UNC name
prefix = "\\\\?\\UNC"
long_path = prefix + path[1:]
else:
# Likely a local path
long_path = prefix + path
elif path.startswith(prefix + "UNC\\"):
# UNC name with long-path prefix
prefix = "\\\\?\\UNC"
return prefix, long_path
def get_awaited_path_from_prefix(prefix, path):
"""Guess the correct path to pass to the SHFileOperationW() call.
The long-path prefix must be removed, so we should take care of
different long-path prefixes.
"""
if prefix == "\\\\?\\UNC":
# We need to prepend a backslash for UNC names, as it was removed
# in prefix_and_path().
return "\\" + path[len(prefix) :]
return path[len(prefix) :]
def get_short_path_name(long_name):
prefix, long_path = prefix_and_path(long_name)
buf_size = GetShortPathNameW(long_path, None, 0)
# FIX: https://github.com/hsoft/send2trash/issues/31
# If buffer size is zero, an error has occurred.
if not buf_size:
err_no = GetLastError()
raise WindowsError(err_no, FormatError(err_no), long_path)
output = create_unicode_buffer(buf_size)
GetShortPathNameW(long_path, output, buf_size)
return get_awaited_path_from_prefix(prefix, output.value)
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
paths = [op.abspath(path) if not op.isabs(path) else path for path in paths]
# get short path to handle path length issues
paths = [get_short_path_name(path) for path in paths]
fileop = SHFILEOPSTRUCTW()
fileop.hwnd = 0
fileop.wFunc = FO_DELETE
# FIX: https://github.com/hsoft/send2trash/issues/17
# Starting in python 3.6.3 it is no longer possible to use:
# LPCWSTR(path + '\0') directly as embedded null characters are no longer
# allowed in strings
# Workaround
# - create buffer of c_wchar[] (LPCWSTR is based on this type)
# - buffer is two c_wchar characters longer (double null terminator)
# - cast the address of the buffer to a LPCWSTR
# NOTE: based on how python allocates memory for these types they should
# always be zero, if this is ever not true we can go back to explicitly
# setting the last two characters to null using buffer[index] = '\0'.
# Additional note on another issue here, unicode_buffer expects length in
# bytes essentially, so having multi-byte characters causes issues if just
# passing pythons string length. Instead of dealing with this difference we
# just create a buffer then a new one with an extra null. Since the non-length
# specified version apparently stops after the first null, join with a space first.
buffer = create_unicode_buffer(" ".join(paths))
# convert to a single string of null terminated paths
path_string = "\0".join(paths)
buffer = create_unicode_buffer(path_string, len(buffer) + 1)
fileop.pFrom = LPCWSTR(addressof(buffer))
fileop.pTo = None
fileop.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT
fileop.fAnyOperationsAborted = 0
fileop.hNameMappings = 0
fileop.lpszProgressTitle = None
result = SHFileOperationW(byref(fileop))
if result:
error = convert_sh_file_opt_result(result)
raise WindowsError(None, FormatError(error), paths, error)

View File

@ -6,43 +6,40 @@
from __future__ import unicode_literals
import os.path as op
from .compat import text_type
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 send2trash.win.IFileOperationProgressSink import create_sink
def send2trash(paths):
if not isinstance(paths, list):
paths = [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
]
paths = [text_type(path, "mbcs") if not isinstance(path, text_type) else path for path in paths]
# convert to full paths
paths = [op.abspath(path) if not op.isabs(path) else path for path in paths]
# remove the leading \\?\ if present
paths = [path[4:] if path.startswith("\\\\?\\") else path for path in paths]
# Need to initialize the com before using
pythoncom.CoInitialize()
# create instance of file operation object
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
)
flags = shellcon.FOF_NOCONFIRMATION | shellcon.FOF_NOERRORUI | shellcon.FOF_SILENT | shellcon.FOFX_EARLYFAILURE
# determine rest of the flags based on OS version
# use newer recommended flags if available
if int(version().split(".", 1)[0]) >= 8:
flags |= (
0x20000000 # FOFX_ADDUNDORECORD win 8+
| 0x00080000 # FOFX_RECYCLEONDELETE win 8+
)
flags |= 0x20000000 | 0x00080000 # FOFX_ADDUNDORECORD win 8+ # FOFX_RECYCLEONDELETE win 8+
else:
flags |= shellcon.FOF_ALLOWUNDO
# set the flags
@ -50,10 +47,11 @@ 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 = create_sink()
try:
for path in paths:
item = shell.SHCreateItemFromParsingName(path, None, shell.IID_IShellItem)
fileop.DeleteItem(item)
fileop.DeleteItem(item, sink)
result = fileop.PerformOperations()
aborted = fileop.GetAnyOperationsAborted()
# if non-zero result or aborted throw an exception
@ -63,3 +61,6 @@ def send2trash(paths):
# convert to standard OS error, allows other code to get a
# normal errno
raise OSError(None, error.strerror, path, error.hresult)
finally:
# Need to make sure we call this once fore every init
pythoncom.CoUninitialize()

51
setup.cfg Normal file
View File

@ -0,0 +1,51 @@
[metadata]
name = Send2Trash
version = 1.8.3
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.7
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
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.*, !=3.5.*, !=3.6.*
[options.packages.find]
include=
send2trash*
[options.extras_require]
win32 =
pywin32; sys_platform == "win32"
objc =
pyobjc-framework-Cocoa; sys_platform == "darwin"
nativeLib =
pywin32; sys_platform == "win32"
pyobjc-framework-Cocoa; sys_platform == "darwin"
[options.entry_points]
console_scripts =
send2trash = send2trash.__main__:main

View File

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

View File

@ -1,18 +0,0 @@
import sys
import unittest
def TestSuite():
suite = unittest.TestSuite()
loader = unittest.TestLoader()
if sys.platform == "win32":
from . import test_plat_win
suite.addTests(loader.loadTestsFromModule(test_plat_win))
else:
from . import test_script_main
from . import test_plat_other
suite.addTests(loader.loadTestsFromModule(test_script_main))
suite.addTests(loader.loadTestsFromModule(test_plat_other))
return suite

View File

@ -1,25 +1,79 @@
# encoding: utf-8
import pytest
import codecs
import unittest
import os
import sys
from os import path as op
import send2trash.plat_other
from send2trash.plat_other import send2trash as s2t
from send2trash.compat import PY3
from send2trash import TrashPermissionError
try:
from configparser import ConfigParser
except ImportError:
# py2
from ConfigParser import ConfigParser
from tempfile import mkdtemp, NamedTemporaryFile, mktemp
from ConfigParser import ConfigParser # noqa: F401
from tempfile import mkdtemp, NamedTemporaryFile
import shutil
import stat
import sys
import uuid
# Could still use cleaning up. But no longer relies on ramfs.
if sys.platform != "win32":
import send2trash.plat_other
from send2trash.plat_other import send2trash as s2t
HOMETRASH = send2trash.plat_other.HOMETRASH
INFO_SUFFIX = send2trash.plat_other.INFO_SUFFIX.decode()
HOMETRASH = send2trash.plat_other.HOMETRASH
else:
pytest.skip("Skipping non-windows tests", allow_module_level=True)
@pytest.fixture
def testfile():
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(
map(
lambda index: NamedTemporaryFile(
dir=op.expanduser("~"),
prefix="send2trash_test{}".format(index),
delete=False,
),
range(10),
)
)
[file.close() for file in files]
assert all([op.exists(file.name) for file in files]) is True
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 + INFO_SUFFIX)) for filename in filenames]
def test_trash(testfile):
s2t(testfile.name)
assert op.exists(testfile.name) is False
def test_multitrash(testfiles):
filenames = [file.name for file in testfiles]
s2t(filenames)
assert any([op.exists(filename) for filename in filenames]) is False
def touch(path):
@ -27,226 +81,151 @@ def touch(path):
os.utime(path, None)
class TestHomeTrash(unittest.TestCase):
def setUp(self):
self.file = NamedTemporaryFile(
dir=op.expanduser("~"), prefix="send2trash_test", delete=False
)
def test_trash(self):
s2t(self.file.name)
self.assertFalse(op.exists(self.file.name))
def tearDown(self):
name = op.basename(self.file.name)
os.remove(op.join(HOMETRASH, "files", name))
os.remove(op.join(HOMETRASH, "info", name + ".trashinfo"))
class TestHomeMultiTrash(unittest.TestCase):
def setUp(self):
self.files = list(
map(
lambda index: NamedTemporaryFile(
dir=op.expanduser("~"),
prefix="send2trash_test{}".format(index),
delete=False,
),
range(10),
)
)
def test_multitrash(self):
filenames = [file.name for file in self.files]
s2t(filenames)
self.assertFalse(any([op.exists(filename) for filename in filenames]))
def tearDown(self):
filenames = [op.basename(file.name) for file in self.files]
[os.remove(op.join(HOMETRASH, "files", filename)) for filename in filenames]
[
os.remove(op.join(HOMETRASH, "info", filename + ".trashinfo"))
for filename in filenames
]
def _filesys_enc():
enc = sys.getfilesystemencoding()
# Get canonical name of codec
return codecs.lookup(enc).name
@unittest.skipIf(_filesys_enc() == "ascii", "ASCII filesystem")
class TestUnicodeTrash(unittest.TestCase):
def setUp(self):
self.name = u"send2trash_tést1"
self.file = op.join(op.expanduser(b"~"), self.name.encode("utf-8"))
touch(self.file)
def test_trash_bytes(self):
s2t(self.file)
assert not op.exists(self.file)
def test_trash_unicode(self):
s2t(self.file.decode(sys.getfilesystemencoding()))
assert not op.exists(self.file)
def tearDown(self):
if op.exists(self.file):
os.remove(self.file)
trash_file = op.join(HOMETRASH, "files", self.name)
if op.exists(trash_file):
os.remove(trash_file)
os.remove(op.join(HOMETRASH, "info", self.name + ".trashinfo"))
@pytest.fixture
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" 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)
#
# Tests for files on some other volume than the user's home directory.
#
# What we need to stub:
# * plat_other.get_dev (to make sure the file will not be on the home dir dev)
# * os.path.ismount (to make our topdir look like a top dir)
#
class TestExtVol(unittest.TestCase):
def setUp(self):
self.trashTopdir = mkdtemp(prefix="s2t")
@pytest.mark.skipif(_filesys_enc() == "ascii", reason="Requires Unicode filesystem")
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(gen_unicode_file):
s2t(gen_unicode_file.decode(sys.getfilesystemencoding()))
assert not op.exists(gen_unicode_file)
class ExtVol:
def __init__(self, path):
self.trash_topdir = path
if PY3:
trashTopdir_b = os.fsencode(self.trashTopdir)
self.trash_topdir_b = os.fsencode(self.trash_topdir)
else:
trashTopdir_b = self.trashTopdir
self.fileName = "test.txt"
self.filePath = op.join(self.trashTopdir, self.fileName)
touch(self.filePath)
self.old_ismount = old_ismount = op.ismount
self.old_getdev = send2trash.plat_other.get_dev
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(trashTopdir_b),
op.realpath(self.trash_topdir),
op.realpath(self.trash_topdir_b),
):
return True
return old_ismount(path)
self.old_ismount = old_ismount = op.ismount
self.old_getdev = send2trash.plat_other.get_dev
send2trash.plat_other.os.path.ismount = s_ismount
send2trash.plat_other.get_dev = s_getdev
def tearDown(self):
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)
class TestTopdirTrash(TestExtVol):
def setUp(self):
TestExtVol.setUp(self)
# Create a .Trash dir w/ a sticky bit
self.trashDir = op.join(self.trashTopdir, ".Trash")
os.mkdir(self.trashDir, 0o777 | stat.S_ISVTX)
@pytest.fixture
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(self):
s2t(self.filePath)
self.assertFalse(op.exists(self.filePath))
self.assertTrue(
op.exists(op.join(self.trashDir, str(os.getuid()), "files", self.fileName))
)
self.assertTrue(
op.exists(
op.join(
self.trashDir,
str(os.getuid()),
"info",
self.fileName + ".trashinfo",
)
)
)
# info relative path (if another test is added, with the same fileName/Path,
# then it gets renamed etc.)
cfg = ConfigParser()
cfg.read(
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(
self.trashDir, str(os.getuid()), "info", self.fileName + ".trashinfo"
trash_dir,
str(os.getuid()),
"info",
gen_ext_vol[1] + INFO_SUFFIX,
)
)
self.assertEqual(self.fileName, cfg.get("Trash Info", "Path", raw=True))
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(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
# Test .Trash-UID
class TestTopdirTrashFallback(TestExtVol):
def test_trash(self):
touch(self.filePath)
s2t(self.filePath)
self.assertFalse(op.exists(self.filePath))
self.assertTrue(
op.exists(
op.join(
self.trashTopdir,
".Trash-" + str(os.getuid()),
"files",
self.fileName,
)
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
)
# Test failure
class TestTopdirFailure(TestExtVol):
def setUp(self):
TestExtVol.setUp(self)
os.chmod(self.trashTopdir, 0o500) # not writable to induce the exception
def test_trash(self):
with self.assertRaises(OSError):
s2t(self.filePath)
self.assertTrue(op.exists(self.filePath))
def tearDown(self):
os.chmod(self.trashTopdir, 0o700) # writable to allow deletion
TestExtVol.tearDown(self)
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
# Make sure it will find the mount point properly for a file in a symlinked path
class TestSymlink(TestExtVol):
def setUp(self):
TestExtVol.setUp(self)
# Use mktemp (race conditioney but no symlink equivalent)
# Since is_parent uses realpath(), and our getdev uses is_parent,
# this should work
self.slDir = mktemp(prefix="s2t", dir=op.expanduser("~"))
os.mkdir(op.join(self.trashTopdir, "subdir"), 0o700)
self.filePath = op.join(self.trashTopdir, "subdir", self.fileName)
touch(self.filePath)
os.symlink(op.join(self.trashTopdir, "subdir"), self.slDir)
def test_trash(self):
s2t(op.join(self.slDir, self.fileName))
self.assertFalse(op.exists(self.filePath))
self.assertTrue(
op.exists(
op.join(
self.trashTopdir,
".Trash-" + str(os.getuid()),
"files",
self.fileName,
)
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],
)
)
def tearDown(self):
os.remove(self.slDir)
TestExtVol.tearDown(self)
if __name__ == "__main__":
unittest.main()
is True
)
os.remove(sl_dir)

View File

@ -1,154 +1,220 @@
# coding: utf-8
# encoding: utf-8
import os
import shutil
import sys
import unittest
import pytest
from os import path as op
from tempfile import gettempdir
from send2trash import send2trash as s2t
# import the two versions as well as the "automatic" version
from send2trash.plat_win_modern import send2trash as s2t_modern
from send2trash.plat_win_legacy import send2trash as s2t_legacy
if sys.platform == "win32":
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)
@unittest.skipIf(sys.platform != "win32", "Windows only")
class TestNormal(unittest.TestCase):
def setUp(self):
self.dirname = "\\\\?\\" + op.join(gettempdir(), "python.send2trash")
self.file = op.join(self.dirname, "testfile.txt")
self._create_tree(self.file)
self.files = [
op.join(self.dirname, "testfile{}.txt".format(index)) for index in range(10)
]
[self._create_tree(file) for file in self.files]
def tearDown(self):
shutil.rmtree(self.dirname, ignore_errors=True)
def _create_tree(self, path):
dirname = op.dirname(path)
if not op.isdir(dirname):
os.makedirs(dirname)
with open(path, "w") as writer:
writer.write("send2trash test")
def _trash_file(self, fcn):
fcn(self.file)
self.assertFalse(op.exists(self.file))
def _trash_multifile(self, fcn):
fcn(self.files)
self.assertFalse(any([op.exists(file) for file in self.files]))
def _file_not_found(self, fcn):
file = op.join(self.dirname, "otherfile.txt")
self.assertRaises(WindowsError, fcn, file)
def test_trash_file(self):
self._trash_file(s2t)
def test_trash_multifile(self):
self._trash_multifile(s2t)
def test_file_not_found(self):
self._file_not_found(s2t)
def test_trash_file_modern(self):
self._trash_file(s2t_modern)
def test_trash_multifile_modern(self):
self._trash_multifile(s2t_modern)
def test_file_not_found_modern(self):
self._file_not_found(s2t_modern)
def test_trash_file_legacy(self):
self._trash_file(s2t_legacy)
def test_trash_multifile_legacy(self):
self._trash_multifile(s2t_legacy)
def test_file_not_found_legacy(self):
self._file_not_found(s2t_legacy)
def _create_tree(path):
dirname = op.dirname(path)
if not op.isdir(dirname):
os.makedirs(dirname)
with open(path, "w") as writer:
writer.write("send2trash test")
@unittest.skipIf(sys.platform != "win32", "Windows only")
class TestLongPath(unittest.TestCase):
def setUp(self):
self.functions = {s2t: "auto", s2t_legacy: "legacy", s2t_modern: "modern"}
filename = "A" * 100
self.dirname = "\\\\?\\" + op.join(gettempdir(), filename)
path = op.join(
self.dirname,
filename,
filename, # From there, the path is not trashable from Explorer
filename,
filename + "{}.txt",
)
self.file = path.format("")
self._create_tree(self.file)
self.files = [path.format(index) for index in range(10)]
[self._create_tree(file) for file in self.files]
@pytest.fixture
def testdir(tmp_path):
dirname = "\\\\?\\" + str(tmp_path)
assert op.exists(dirname) is True
yield dirname
shutil.rmtree(dirname, ignore_errors=True)
def tearDown(self):
shutil.rmtree(self.dirname, ignore_errors=True)
def _create_tree(self, path):
dirname = op.dirname(path)
if not op.isdir(dirname):
os.makedirs(dirname)
with open(path, "w") as writer:
writer.write("Looong filename!")
@pytest.fixture
def testfile(testdir):
file = op.join(testdir, "testfile.txt")
_create_tree(file)
assert op.exists(file) is True
yield file
# Note dir will cleanup the file
def _trash_file(self, fcn):
fcn(self.file)
self.assertFalse(op.exists(self.file))
def _trash_multifile(self, fcn):
fcn(self.files)
self.assertFalse(any([op.exists(file) for file in self.files]))
@pytest.fixture
def testfiles(testdir):
files = [op.join(testdir, "testfile{}.txt".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 dir will cleanup the files
def _trash_folder(self, fcn):
fcn(self.dirname)
self.assertFalse(op.exists(self.dirname))
def test_trash_file(self):
self._trash_file(s2t)
def _trash_folder(dir, fcn):
fcn(dir)
assert op.exists(dir) is False
def test_trash_multifile(self):
self._trash_multifile(s2t)
@unittest.skipIf(
op.splitdrive(os.getcwd())[0] != op.splitdrive(gettempdir())[0],
"Cannot trash long path from other drive",
)
def test_trash_folder(self):
self._trash_folder(s2t)
def _trash_file(file, fcn):
fcn(file)
assert op.exists(file) is False
def test_trash_file_modern(self):
self._trash_file(s2t_modern)
def test_trash_multifile_modern(self):
self._trash_multifile(s2t_modern)
def _trash_multifile(files, fcn):
fcn(files)
assert any([op.exists(file) for file in files]) is False
@unittest.skipIf(
op.splitdrive(os.getcwd())[0] != op.splitdrive(gettempdir())[0],
"Cannot trash long path from other drive",
)
def test_trash_folder_modern(self):
self._trash_folder(s2t_modern)
def test_trash_file_legacy(self):
self._trash_file(s2t_legacy)
def _file_not_found(dir, fcn):
file = op.join(dir, "otherfile.txt")
pytest.raises(OSError, fcn, file)
def test_trash_multifile_legacy(self):
self._trash_multifile(s2t_legacy)
@unittest.skipIf(
op.splitdrive(os.getcwd())[0] != op.splitdrive(gettempdir())[0],
"Cannot trash long path from other drive",
)
def test_trash_folder_legacy(self):
self._trash_folder(s2t_legacy)
def _multi_byte_unicode(dir, fcn):
single_file = op.join(dir, "😇.txt")
_create_tree(single_file)
assert op.exists(single_file) is True
fcn(single_file)
assert op.exists(single_file) is False
files = [op.join(dir, "😇{}.txt".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
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)
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
# 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
# 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):
_trash_file(longfile, s2t_modern)
def test_trash_long_multifile_modern(longfiles):
_trash_multifile(longfiles, 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():
try:
s2t_modern([])
except Exception as ex:
assert False, "Exception thrown when trashing nothing: {}".format(ex)

View File

@ -1,32 +1,41 @@
# encoding: utf-8
import os
import unittest
import sys
import pytest
from tempfile import NamedTemporaryFile
from os import path as op
from send2trash.__main__ import main as trash_main
from tests.test_plat_other import HOMETRASH
# Only import HOMETRASH on supported platforms
if sys.platform != "win32":
from send2trash.plat_other import HOMETRASH
class TestMainTrash(unittest.TestCase):
def setUp(self):
self.file = NamedTemporaryFile(dir=op.expanduser('~'), prefix='send2trash_test', delete=False)
def test_trash(self):
trash_main(['-v', self.file.name])
self.assertFalse(op.exists(self.file.name))
def test_no_args(self):
self.assertRaises(SystemExit, trash_main, [])
self.assertRaises(SystemExit, trash_main, ['-v'])
self.assertTrue(op.exists(self.file.name))
trash_main([self.file.name]) # Trash the file so tearDown runs properly
def tearDown(self):
name = op.basename(self.file.name)
os.remove(op.join(HOMETRASH, 'files', name))
os.remove(op.join(HOMETRASH, 'info', name + '.trashinfo'))
@pytest.fixture
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)
if __name__ == '__main__':
unittest.main()
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, ["-v"])
assert op.exists(file) is True

22
tox.ini
View File

@ -1,23 +1,25 @@
[tox]
envlist = py27,py34,py35,py36,py3-win
envlist = py{27,34,35,36,37,38,39,310}
skip_missing_interpreters = True
isolated_build = True
[testenv]
platform = linux
deps =
flake8
pytest
pywin32; sys_platform == 'win32'
pyobjc-framework-Cocoa; sys_platform == 'darwin'
commands =
python setup.py test --test-suite tests.TestSuite
[testenv:py3-win]
platform = win
commands =
python -m pip install pywin32
python setup.py test --test-suite tests.TestSuite
flake8
pytest
[testenv:py27]
deps =
configparser
{[testenv]deps}
[flake8]
exclude = .tox,env,build
max-line-length = 120
ignore = E731,E203,E501,W503
select = C,E,F,W,B,B950
extend-ignore = E203, E501