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

116 Commits
1.0.0 ... 1.8.0

Author SHA1 Message Date
484913ba0f Update version for 1.8.0 release 2021-08-08 21:51:06 -05:00
d249f0106b Fix #59, initialize and uninitialize COM for threading 2021-08-07 22:16:33 -05:00
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
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
6612545110 Add note about pyobjc to README, add extra option 2021-06-22 21:36:14 -05:00
d52b4f206c Fix CHANGES.rst issue 2021-06-21 22:22:48 -05:00
33171dde82 Update version for 1.7.1 release 2021-06-21 22:13:46 -05:00
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
23ce7b8c16 Bump version 2021-05-14 21:44:21 -05:00
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
c8bcaea1e8 Update version and changelog for release 2021-04-20 17:35:59 -05:00
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
10c7693d11 Minor fixes to tests 2021-03-17 21:51:51 -05:00
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
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
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
37be84d46e Update .travis.yml 2021-03-02 22:48:48 -06:00
9f76fbf036 Updates for tests on windows
- Other platform tests WIP
- Windows long directory tests WIP
2021-03-02 19:33:44 -06:00
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
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
054d56c564 Update Tox and Travis configurations 2021-03-02 00:24:59 -06:00
33ed07811b Cleanup flake8 issues 2021-03-01 23:44:03 -06:00
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
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
60bcb2c834 Merge pull request #47 from pracedru/master
Update plat_other.py
2021-01-21 19:00:46 -06:00
c411f4eae4 Merge branch 'master' into master 2021-01-21 19:00:33 -06:00
f64c69f905 Merge branch 'master' into master 2021-01-21 18:57:39 -06:00
00dfe77e40 Add console_script entry point, close #50 2021-01-12 18:22:23 -06:00
16a7115ff1 Merge pull request #52 from BoboTiG/impr-expand-ci
Expand supported Python versions
2021-01-12 16:58:09 -06:00
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
49bc438546 Update files to release a new package 2020-06-18 21:49:42 -05:00
2e9fa38f56 Fix some flake8 errors and cleanup
- Fix some flake8 formatting errors
- Auto-format files edited
- Also ignore some flake8 errors when they are intented
- Update .gitignore to ingore local env
2020-06-18 21:47:06 -05:00
1e099724c5 Fix a typo in tox.ini for windows 2020-06-11 23:23:54 -05:00
6ac20bc4f6 Fix new issues with unit tests
- Create custom test suite based on platform
- Use the custom suite for all tests
- Update tox.ini to install pywin32
2020-06-11 23:20:16 -05:00
e3d2be3243 Update windows tests to test both versions
This fixes #44 by testing the automatic import, the legacy version, and
the modern version directtly.
2020-06-11 22:42:00 -05:00
d078554052 Windows Performance Improvement & Multi-Item support (#43)
* Initial IFileOperation for Windows

- Try using IFileOperation instead of SHFileOperation
  - Use pywin32 to accomplish this
  - Implement fallback when pywin32 not available
- Handles paths like `C:\` just fine bu the `\\?\` paths in the test
  cause issue
- Add batching for IFileOperation version (performance)
- Minor formatting applied by editor

* Fix issue with paths starting with \\?\

- Strip these characters off if present just like old implementation

* Add windows version check, legacy list support

- Add check for windows version for IFileOperation
- Add list support to legacy version
- Remove some debugging code
- Fix bug in path converson

Not sure if there is a better way to layout this file

* Split plat_win into legacy and modern

* Update other platforms for list support

Formatter also ran on these so some other minor changes.

* Add unit tests for multi-file calls
2020-06-03 12:49:41 -04:00
Matthew D. Scholefield
9ede898c3e Create __main__.py (Fixes #15) (#38)
This adds a main method that mimics the behavior of `rm`. It can be called via `python -m send2trash somefile`.
2020-05-27 07:52:00 -04:00
sharkykh
66afce7252 Fix silently failing on Windows (#33)
* Fix #31: Silently failing on Windows

* Update Windows test

* Fix test folders not getting removed
2019-04-30 12:28:09 -04:00
sharkykh
8f684a9c8b Add Python 3.7 and Windows to Travis-CI (#34)
* Add Python 3.7 and Windows to Travis-CI

* Reorder environments
2019-04-30 12:22:28 -04:00
Virgil Dupras
1c32d471f2 README: Maintainer wanted 2019-01-04 10:27:27 -05:00
Virgil Dupras
74352462f5 Fix broken tests on py2 2018-07-26 08:30:39 -04:00
Virgil Dupras
0d7b4b4ad9 v1.5.0 2018-02-16 09:57:27 -05:00
Thomas Kluyver
1dded4f572 Raise TrashPermissionError from gio backend (#22) 2018-02-16 09:30:26 -05:00
Mickaël Schoentgen
020d05979d Windows: Workaround for long paths (#23)
By using the short path version of a file, we can
manage to move long paths to the trash.

Limitations:
1/ If the final short path is longer than what
    `SHFileOperationW` can handle, it will fail
2/ Still not able to trash long path from another
    drive, ie: trying to delete C:\temp\foo.txt
    while the script is running from D:\trash.py
2018-02-16 09:07:05 -05:00
Thomas Kluyver
6b0bd46036 Define TrashPermissionError (#21) 2018-02-06 17:28:47 -05:00
Nicholas Bollweg
f6897609ba Include LICENSE in package (#19) 2018-01-06 08:19:31 -05:00
Virgil Dupras
7d38de269a v1.4.2 2017-11-17 13:50:29 -05:00
5733670fc2 Workaround embedded null character (#18)
Workaround embedded null characters in strings.  This fixes 17.
2017-11-01 08:28:22 -04:00
Virgil Dupras
22ed5dc09b v1.4.1 2017-08-07 23:12:36 -04:00
Virgil Dupras
3071684f73 Fix newly-introduced crash under Windows
ref #14
2017-08-07 22:51:37 -04:00
Virgil Dupras
75a9cd6b55 v1.4.0 2017-08-07 21:32:38 -04:00
Virgil Dupras
f38aec6569 Reindent test_plat_other 2017-08-03 21:03:34 -04:00
Virgil Dupras
072e28cb1e Update classification in setup.py 2017-08-03 20:52:34 -04:00
Virgil Dupras
016b90c1ac Update copyright 2017-08-03 20:52:19 -04:00
Virgil Dupras
f324ff491e Properly reuse the "compat" unit 2017-08-03 20:47:58 -04:00
Virgil Dupras
f3231ef857 Merge branch 'unicode-trash' of https://github.com/takluyver/send2trash into takluyver-unicode-trash 2017-08-03 20:37:13 -04:00
Virgil Dupras
b7e3057853 Fix tests, add tox.ini and travis.yml 2017-08-03 20:34:10 -04:00
Thomas Kluyver
7fece243d8 Use bytes throughout plat_other 2017-08-01 12:26:09 +01:00
Thomas Kluyver
4181ed65e9 Add failing test (on Python 2) for unicode file names 2017-08-01 11:28:40 +01:00
Thomas Kluyver
dd69edad3b Fix test on Python 3 2017-08-01 11:07:04 +01:00
Virgil Dupras
bd9183afe9 v1.3.1 2017-07-31 14:21:18 -04:00
Kfir Hadas
f6f63b1796 Use text_type (unicode for PY2, str for PY3) (#12) 2017-07-07 16:09:16 -04:00
Virgil Dupras
0974912e78 Merge pull request #7 from julian-r/master
throwing a WindowsError with the code
2016-06-04 19:22:46 -04:00
Julian David Rath
6c01453fd3 throwing a WindowsError with the code 2016-04-12 08:53:04 +02:00
Virgil Dupras
7cbefa4317 Merge pull request #6 from glensc/patch-1
Update plat_other.py
2016-04-10 19:04:38 -04:00
Elan Ruusamäe
72bc94b48d Update plat_other.py
minor typo fix
2016-04-10 11:09:46 +03:00
Virgil Dupras
35ad95bcd5 Fixed typo in changelog 2013-07-19 19:33:09 -04:00
Virgil Dupras
a568370c6a v1.3.0 2013-07-19 19:26:34 -04:00
Virgil Dupras
baf125ff61 Added support for Gnome's gio
Instead of using our own Freedesktop's trash implementation,
use gio when it's available.
2013-07-19 19:16:11 -04:00
Virgil Dupras
bb8ed834da Add same-codebase support for python 2.7.
When I opted for two codebases for python2/python3,
Send2Trash used C modules and it was easier to just have two
packages. With the ctypes version, supporting both python
versions becomes trivial and it's much more convenient to
merge them back into a single codebase.

I've only tested this code on Linux. I've converted plat_osx and
plat_win, but they only work theoretically.
2013-07-19 18:42:32 -04:00
Virgil Dupras
a8dbb1ac63 Renamed CHANGES --> CHANGES.rst
And fixed setup.py which was broken since README rename.
2013-07-19 18:23:00 -04:00
Virgil Dupras
bfd8f6e024 Updated repo URL to point to GH. 2013-07-19 18:18:16 -04:00
Virgil Dupras
8996fb9eac Renamed README --> README.rst 2013-07-19 18:14:27 -04:00
Virgil Dupras
f7a6f217ce Converted to git. 2013-07-19 18:12:18 -04:00
Virgil Dupras
a4936be846 Added tag 1.2.0 for changeset 2cba92b88b1d 2011-03-16 10:27:52 +01:00
Virgil Dupras
0f95d7506e v1.2.0 2011-03-16 10:27:41 +01:00
gbn
b415ac86e3 Modification to symlink test case (that will actually fail when it should -- find_mount_point using abspath instead of realpath.) 2011-03-13 15:35:14 -04:00
gbn
eeaf4e8ffa Add a test case for a path containing a symlink. 2011-03-13 15:17:13 -04:00
gbn
798893215c Make the (still ugly) test no longer rely on ramfs/being root 2011-03-13 14:43:24 -04:00
gbn
aee2b7a8af Check access and devices before attempting trash. 2011-03-13 14:40:52 -04:00
gbn
d090156c45 Use realpath to find mountpoint 2011-03-13 14:38:03 -04:00
Virgil Dupras
358b705cbc Made a few minor style fixes, and added a proper error in cases where the target path of send2trash() doesn't exist. 2011-03-12 11:48:19 +01:00
gbn
eedbe258cb URL Escape the Path in trashinfo 2011-03-10 14:56:19 -05:00
gbn
18e3187c2f Replace == None with is None 2011-03-10 12:22:21 -05:00
gbn
8001be8f37 Remove import * 2011-03-10 12:21:05 -05:00
gbn
13b3943c82 Replace plat_other with one supporting the XDG Trash spec
Added tests for plat_other
2011-03-10 04:55:46 -05:00
Virgil Dupras
a8a771c9bd Merged default branch with py3k. Py3k version of send2trash is now the default one (python 2 version is in the py2k branch). 2011-02-14 11:00:09 +01:00
Virgil Dupras
9189e685b1 Adjusted packaging metadata for 1.1.0.
--HG--
branch : py3k
2010-10-18 12:22:13 +02:00
Virgil Dupras
04ee6eaf9f Added tag 1.1.0 for changeset de5f43fcce5e
--HG--
branch : py3k
2010-10-18 12:13:42 +02:00
Virgil Dupras
506e48b2e0 v1.1.0
--HG--
branch : py3k
2010-10-18 12:13:37 +02:00
Virgil Dupras
899a3efeb3 Converted the compiled win module to ctypes.
--HG--
branch : py3k
2010-10-17 18:00:56 +01:00
Virgil Dupras
a6907d57a9 Converted the compiled osx module to ctypes.
--HG--
branch : py3k
2010-10-17 18:28:28 +02:00
Virgil Dupras
51d8a51cb7 Added a setuptools-crappiness notice in the README. 2010-07-13 12:19:19 +02:00
Virgil Dupras
b5315cb73d Added a setuptools-crappiness notice in the README.
--HG--
branch : py3k
extra : transplant_source : %F8%9A%02%C3%D2%AA%AB%2C%5D%94%EA%13%BD%D6%A0U%3F%D2N%C9
2010-07-13 12:19:19 +02:00
Virgil Dupras
86450a3dee v1.0.2 2010-07-10 07:07:42 +02:00
Virgil Dupras
d1fcb13086 v1.0.2
--HG--
branch : py3k
2010-07-10 07:06:01 +02:00
Virgil Dupras
31907c9c4a Fixed a bug in plat_other where conflict handling wouldn't be done correctly in external volume. Thanks to John Benediktsson for the tip. 2010-07-09 21:49:46 -07:00
Virgil Dupras
02dc392c45 Fixed a bug in plat_other where conflict handling wouldn't be done correctly in external volume. Thanks to John Benediktsson for the tip.
--HG--
branch : py3k
extra : transplant_source : %C6%11%009sx%B29%CF%EC%CC%D4%88r%BE%D8%BB%9AIa
2010-07-09 21:49:46 -07:00
Virgil Dupras
2572a7c00c Fixed an infinite loop in plat_other when using a relative path in a mounted directory. 2010-07-09 21:46:19 -07:00
Virgil Dupras
7546aa606a Fixed an infinite loop in plat_other when using a relative path in a mounted directory.
--HG--
branch : py3k
extra : transplant_source : %B4%A2%DB%EFn%BB%3A%F6%AE%06%F3%29%DB%06%FBE%D0%A2%BEt
2010-07-09 21:46:19 -07:00
Virgil Dupras
2858b5b153 Converted to py3k (haven't tried it on Windows yet, but it should compile and work...)
--HG--
branch : py3k
2010-07-07 16:12:13 +02:00
Virgil Dupras
88b90d859c Updated package metadata. 2010-07-07 11:59:11 +02:00
Virgil Dupras
f5f9c5b352 Fixed the copyright comment which was at a strange place. 2010-04-21 10:30:51 +02:00
Virgil Dupras
06f03e14b4 Fixed spaces/tabs mixup. 2010-04-21 10:14:46 +02:00
Virgil Dupras
8313b0eebb Added tag 1.0.1 for changeset a7e04d8e47e1 2010-04-19 11:25:35 +02:00
Virgil Dupras
e78d1d1bd9 Merged heads. 2010-04-19 11:25:02 +02:00
Virgil Dupras
de898fdcaa v1.0.1: Fixed memory leak. 2010-04-19 11:24:06 +02:00
Virgil Dupras
a3e41602cf Added long_description to setup. 2010-04-08 16:28:23 +02:00
Virgil Dupras
8b00632dd6 Set zip_safe to False, as it causes problems when creating executables for Windows of apps using it. 2010-04-07 16:25:01 +01:00
Virgil Dupras
fa68152b35 Added tag 1.0.0 for changeset 48c2103380f5 2010-04-07 13:16:55 +02:00
31 changed files with 1511 additions and 254 deletions

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
*.pyc
*.egg-info
/build
/dist
.DS_Store
/.tox
__pycache__
/env

View File

@@ -1,5 +0,0 @@
syntax: glob
*.pyc
build
.DS_Store

33
.travis.yml Normal file
View File

@@ -0,0 +1,33 @@
language: python
matrix:
include:
- os: windows
language: sh
python: "3.8"
env: "PATH=/c/Python38:/c/Python38/Scripts:$PATH"
# Perform the manual steps on windows to install python3
before_install:
- choco install python --version=3.8.6
- python -m pip install --upgrade pip
before_script:
- export TOXENV=py38
- python: "2.7"
- python: "3.4"
- python: "3.5"
- python: "3.6"
- python: "3.7"
- python: "3.8"
- python: "3.9"
- python: "nightly" # 3.10
before_script:
- export TOXENV=py310
- python: "2.7"
arch: ppc64le
- python: "3.6"
arch: ppc64le
install:
- python -m pip install tox
before_script:
- export TOXENV=$(echo py$TRAVIS_PYTHON_VERSION | tr -d .)
script:
- python -m tox

View File

@@ -1,4 +0,0 @@
Version 1.0.0 -- 2010/04/07
---
* Initial Release

98
CHANGES.rst Normal file
View File

@@ -0,0 +1,98 @@
Changes
=======
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
-----------------------------
* Add main method which allows calling via ``python -m send2trash somefile``
* Windows: Add support for using IFileOperation when pywin32 is present on Vista and newer
* Add support for passing multiple files at once in a list
* Windows: Batch multi-file calls to improve performance (#42)
* Windows: Fix issue with SHFileOperation failing silently when path is not found (#33)
Version 1.5.0 -- 2018/02/16
---------------------------
* More specific error when failing to create XDG fallback trash directory (#20)
* Windows: Workaround for long paths (#23)
Version 1.4.2 -- 2017/11/17
---------------------------
* Fix incompatibility with Python 3.6 on Windows. (#18)
Version 1.4.1 -- 2017/08/07
---------------------------
* Fix crash on Windows introduced in v1.4.0. Oops... (#14)
Version 1.4.0 -- 2017/08/07
---------------------------
* Use ``bytes`` instead of ``str`` for internal path handling in ``plat_other``. (#13)
Version 1.3.1 -- 2017/07/31
---------------------------
* Throw ``WindowsError`` instead of ``OSError`` in ``plat_win``. (#7)
* Fix ``TypeError`` on python 2 in ``plat_other``. (#12)
Version 1.3.0 -- 2013/07/19
---------------------------
* Added support for Gnome's GIO.
* Merged Python 3 and Python 2 versions in a single codebase.
Version 1.2.0 -- 2011/03/16
---------------------------
* Improved ``plat_other`` to follow freedesktop.org trash specification.
Version 1.1.0 -- 2010/10/18
---------------------------
* Converted compiled modules to ctypes so that cross-platform compilation isn't necessary anymore.
Version 1.0.2 -- 2010/07/10
---------------------------
* Fixed bugs with external volumes in plat_other.
Version 1.0.1 -- 2010/04/19
---------------------------
* Fixed memory leak in OS X module.
Version 1.0.0 -- 2010/04/07
---------------------------
* Initial Release

View File

@@ -1,4 +1,4 @@
Copyright (c) 2010, Hardcoded Software Inc., http://www.hardcoded.net
Copyright (c) 2017, Virgil Dupras
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
@@ -7,4 +7,4 @@ Redistribution and use in source and binary forms, with or without modification,
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
* Neither the name of Hardcoded Software Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

1
MANIFEST.in Normal file
View File

@@ -0,0 +1 @@
include CHANGES.rst LICENSE

25
README
View File

@@ -1,25 +0,0 @@
==================================================
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 (and ugly) ``SHFileOperation`` win32 calls. On other platforms, it moves the file to the first folder it finds that looks like a trash (so far, it's known to work on Ubuntu).
Installation
------------
Download the source from http://hg.hardcoded.net/send2trash and install it with::
>>> sudo python setup.py install
On Windows, you'll need Visual Studio 2008 to compile it. Note that the install you'll get will not be a "universal" package. If you install it on OS X, only the "osx" module will be compiled, and if you install it on Windows, only the "win" module will be compiled.
To have a cross-platform package you can ship around, you'll have compile the package on both platforms and merge the results so that both compiled modules are in the same package.
Usage
-----
>>> from send2trash import send2trash
>>> send2trash('some_file')
When there's a problem ``OSError`` is raised.

56
README.rst Normal file
View File

@@ -0,0 +1,56 @@
==================================================
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 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.
Send2Trash supports Python 2.7 and up (Python 3 is supported).
Status: Additional Help Welcome
-------------------------------
Additional help is welcome for supporting this package. Specifically help with the OSX and Linux
issues and fixes would be most appreciated.
Installation
------------
You can download it with pip:
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::
>>> python setup.py install
Usage
-----
>>> from send2trash import send2trash
>>> send2trash('some_file')
>>> send2trash(['some_file1', 'some_file2'])
On Freedesktop platforms (Linux, BSD, etc.), you may not be able to efficiently
trash some files. In these cases, an exception ``send2trash.TrashPermissionError``
is raised, so that the application can handle this case. This inherits from
``PermissionError`` (``OSError`` on Python 2). Specifically, this affects
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
create a ``.Trash-$UID`` directory.
For any other problem, ``OSError`` is raised.
.. _PyGObject: https://wiki.gnome.org/PyGObject
.. _GIO: https://developer.gnome.org/gio/
.. _trash specifications from freedesktop.org: http://freedesktop.org/wiki/Specifications/trash-spec/

View File

@@ -1,44 +0,0 @@
/* Copyright 2010 Hardcoded Software (http://www.hardcoded.net)
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
*/
#define PY_SSIZE_T_CLEAN
#include "Python.h"
#include <CoreServices/CoreServices.h>
static PyObject* send2trash_osx_send(PyObject *self, PyObject *args)
{
UInt8 *utf8_chars;
FSRef fp;
OSStatus op_result;
if (!PyArg_ParseTuple(args, "es", "utf-8", &utf8_chars)) {
return NULL;
}
FSPathMakeRefWithOptions(utf8_chars, kFSPathMakeRefDoNotFollowLeafSymlink, &fp, NULL);
op_result = FSMoveObjectToTrashSync(&fp, NULL, kFSFileOperationDefaultOptions);
if (op_result != noErr) {
PyErr_SetString(PyExc_OSError, GetMacOSStatusCommentString(op_result));
return NULL;
}
return Py_None;
}
static PyMethodDef TrashMethods[] = {
{"send", send2trash_osx_send, METH_VARARGS, ""},
{NULL, NULL, 0, NULL}
};
PyMODINIT_FUNC
init_send2trash_osx(void)
{
PyObject *m = Py_InitModule("_send2trash_osx", TrashMethods);
if (m == NULL) {
return;
}
}

View File

@@ -1,68 +0,0 @@
/* Copyright 2010 Hardcoded Software (http://www.hardcoded.net)
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
*/
#define PY_SSIZE_T_CLEAN
#include "Python.h"
#define WINDOWS_LEAN_AND_MEAN
#include "windows.h"
#include "shlobj.h"
/* WARNING: If the filepath is not fully qualified, Windows deletes the file
rather than sending it to trash.
*/
static PyObject* send2trash_win_send(PyObject *self, PyObject *args)
{
SHFILEOPSTRUCTW op;
PyObject *filepath;
Py_ssize_t len;
WCHAR filechars[MAX_PATH+1];
int r;
if (!PyArg_ParseTuple(args, "O", &filepath)) {
return NULL;
}
if (!PyUnicode_Check(filepath)) {
PyErr_SetString(PyExc_TypeError, "Unicode filename required");
return NULL;
}
len = PyUnicode_GET_SIZE(filepath);
memcpy(filechars, PyUnicode_AsUnicode(filepath), sizeof(WCHAR)*len);
filechars[len] = '\0';
filechars[len+1] = '\0';
op.hwnd = 0;
op.wFunc = FO_DELETE;
op.pFrom = (LPCWSTR)&filechars;
op.pTo = NULL;
op.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT;
r = SHFileOperationW(&op);
if (r != 0) {
PyErr_Format(PyExc_OSError, "Couldn't perform operation. Error code: %d", r);
return NULL;
}
return Py_None;
}
static PyMethodDef TrashMethods[] = {
{"send", send2trash_win_send, METH_VARARGS, ""},
{NULL, NULL, 0, NULL}
};
PyMODINIT_FUNC
init_send2trash_win(void)
{
PyObject *m = Py_InitModule("_send2trash_win", TrashMethods);
if (m == NULL) {
return;
}
}

View File

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

View File

@@ -1,14 +1,21 @@
# Copyright 2010 Hardcoded Software (http://www.hardcoded.net)
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
# 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
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
import sys
if sys.platform == 'darwin':
from plat_osx import send2trash
elif sys.platform == 'win32':
from plat_win import send2trash
from .exceptions import TrashPermissionError # noqa: F401
if sys.platform == "darwin":
from .plat_osx import send2trash
elif sys.platform == "win32":
from .plat_win import send2trash
else:
from plat_other import send2trash
try:
# If we can use gio, let's use it
from .plat_gio import send2trash
except ImportError:
# Oh well, let's fallback to our own Freedesktop trash implementation
from .plat_other import send2trash # noqa: F401

33
send2trash/__main__.py Normal file
View File

@@ -0,0 +1,33 @@
# 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 __future__ import print_function
import sys
from argparse import ArgumentParser
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')
args = parser.parse_args(args)
for filename in args.files:
try:
send2trash(filename)
if args.verbose:
print('Trashed «' + filename + '»')
except OSError as e:
print(str(e), file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()

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

26
send2trash/exceptions.py Normal file
View File

@@ -0,0 +1,26 @@
import errno
from .compat import PY3
if PY3:
_permission_error = PermissionError # noqa: F821
else:
_permission_error = OSError
class TrashPermissionError(_permission_error):
"""A permission error specific to a trash directory.
Raising this error indicates that permissions prevent us efficiently
trashing a file, although we might still have permission to delete it.
This is *not* used when permissions prevent removing the file itself:
that will be raised as a regular PermissionError (OSError on Python 2).
Application code that catches this may try to simply delete the file,
or prompt the user to decide, or (on Freedesktop platforms), move it to
'home trash' as a fallback. This last option probably involves copying the
data between partitions, devices, or network drives, so we don't do it as
a fallback.
"""
def __init__(self, filename):
_permission_error.__init__(self, errno.EACCES, "Permission denied", filename)

23
send2trash/plat_gio.py Normal file
View File

@@ -0,0 +1,23 @@
# 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 gi.repository import GObject, Gio
from .exceptions import TrashPermissionError
from .util import preprocess_paths
def send2trash(paths):
paths = preprocess_paths(paths)
for path in paths:
try:
f = Gio.File.new_for_path(path)
f.trash(cancellable=None)
except GObject.GError as e:
if e.code == Gio.IOErrorEnum.NOT_SUPPORTED:
# 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.
raise TrashPermissionError("")
raise OSError(e.message)

View File

@@ -1,12 +1,20 @@
# Copyright 2010 Hardcoded Software (http://www.hardcoded.net)
# 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
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
import _send2trash_osx
from platform import mac_ver
from sys import version_info
def send2trash(path):
if not isinstance(path, unicode):
path = unicode(path, 'utf-8')
_send2trash_osx.send(path)
# 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 .plat_osx_pyobjc import send2trash
except ImportError:
# Try to fall back to ctypes version, although likely problematic still
from .plat_osx_ctypes import send2trash
else:
# Just use the old version otherwise
from .plat_osx_ctypes import send2trash # noqa: F401

View File

@@ -0,0 +1,56 @@
# 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
from ctypes import cdll, byref, Structure, c_char, c_char_p
from ctypes.util import find_library
from .compat import binary_type
from .util import preprocess_paths
Foundation = cdll.LoadLibrary(find_library("Foundation"))
CoreServices = cdll.LoadLibrary(find_library("CoreServices"))
GetMacOSStatusCommentString = Foundation.GetMacOSStatusCommentString
GetMacOSStatusCommentString.restype = c_char_p
FSPathMakeRefWithOptions = CoreServices.FSPathMakeRefWithOptions
FSMoveObjectToTrashSync = CoreServices.FSMoveObjectToTrashSync
kFSPathMakeRefDefaultOptions = 0
kFSPathMakeRefDoNotFollowLeafSymlink = 0x01
kFSFileOperationDefaultOptions = 0
kFSFileOperationOverwrite = 0x01
kFSFileOperationSkipSourcePermissionErrors = 0x02
kFSFileOperationDoNotMoveAcrossVolumes = 0x04
kFSFileOperationSkipPreflight = 0x08
class FSRef(Structure):
_fields_ = [("hidden", c_char * 80)]
def check_op_result(op_result):
if op_result:
msg = GetMacOSStatusCommentString(op_result).decode("utf-8")
raise OSError(msg)
def send2trash(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
op_result = FSPathMakeRefWithOptions(path, opts, byref(fp), None)
check_op_result(op_result)
opts = kFSFileOperationDefaultOptions
op_result = FSMoveObjectToTrashSync(byref(fp), None, opts)
check_op_result(op_result)

View File

@@ -0,0 +1,29 @@
# 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 .compat import text_type
from .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

@@ -1,71 +1,211 @@
# 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
# This is a reimplementation of plat_other.py with reference to the
# freedesktop.org trash specification:
# [1] http://www.freedesktop.org/wiki/Specifications/trash-spec
# [2] http://www.ramendik.ru/docs/trashspec.html
# See also:
# [3] http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
#
# For external volumes this implementation will raise an exception if it can't
# find or create the user's trash directory.
from __future__ import unicode_literals
import errno
import sys
import os
import os.path as op
# Copyright 2010 Hardcoded Software (http://www.hardcoded.net)
from datetime import datetime
import stat
# 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
try:
from urllib.parse import quote
except ImportError:
# Python 2
from urllib import quote
import logging
from .compat import text_type, environb
from .util import preprocess_paths
from .exceptions import TrashPermissionError
CANDIDATES = [
'~/.local/share/Trash/files',
'~/.Trash',
]
try:
fsencode = os.fsencode # Python 3
fsdecode = os.fsdecode
except AttributeError:
for candidate in CANDIDATES:
candidate_path = op.expanduser(candidate)
if op.exists(candidate_path):
TRASH_PATH = candidate_path
break
else:
logging.warning("Can't find path for Trash")
TRASH_PATH = op.expanduser('~/.Trash')
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"
INFO_DIR = b"info"
INFO_SUFFIX = b".trashinfo"
# Default of ~/.local/share [3]
XDG_DATA_HOME = op.expanduser(environb.get(b"XDG_DATA_HOME", b"~/.local/share"))
HOMETRASH_B = op.join(XDG_DATA_HOME, b"Trash")
HOMETRASH = fsdecode(HOMETRASH_B)
uid = os.getuid()
TOPDIR_TRASH = b".Trash"
TOPDIR_FALLBACK = b".Trash-" + text_type(uid).encode("ascii")
def is_parent(parent, path):
path = op.realpath(path) # In case it's a symlink
if isinstance(path, text_type):
path = fsencode(path)
parent = op.realpath(parent)
if isinstance(parent, text_type):
parent = fsencode(parent)
return path.startswith(parent)
def format_date(date):
return date.strftime("%Y-%m-%dT%H:%M:%S")
def info_for(src, topdir):
# ...it MUST not include a ".." directory, and for files not "under" that
# directory, absolute pathnames must be used. [2]
if topdir is None or not is_parent(topdir, src):
src = op.abspath(src)
else:
src = op.relpath(src, topdir)
info = "[Trash Info]\n"
info += "Path=" + quote(src) + "\n"
info += "DeletionDate=" + format_date(datetime.now()) + "\n"
return info
def check_create(dir):
# use 0700 for paths [3]
if not op.exists(dir):
os.makedirs(dir, 0o700)
def trash_move(src, dst, topdir=None):
filename = op.basename(src)
filespath = op.join(dst, FILES_DIR)
infopath = op.join(dst, INFO_DIR)
base_name, ext = op.splitext(filename)
counter = 0
destname = filename
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)
with open(op.join(infopath, destname + INFO_SUFFIX), "w") as f:
f.write(info_for(src, topdir))
os.rename(src, op.join(filespath, destname))
EXTERNAL_CANDIDATES = [
'.Trash-1000/files',
'.Trash/files',
'.Trash-1000',
'.Trash',
]
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):
path = op.join(*op.split(path)[:-1])
path = op.split(path)[0]
return path
def find_ext_volume_trash(volume_root):
for candidate in EXTERNAL_CANDIDATES:
candidate_path = op.join(volume_root, candidate)
if op.exists(candidate_path):
return candidate_path
else:
# Something's wrong here. Screw that, just create a .Trash folder
trash_path = op.join(volume_root, '.Trash')
os.mkdir(trash_path)
return trash_path
def move_without_conflict(src, dst):
filename = op.basename(src)
destpath = op.join(dst, filename)
counter = 0
while op.exists(destpath):
counter += 1
base_name, ext = op.splitext(filename)
new_filename = '{0} {1}{2}'.format(base_name, counter, ext)
destpath = op.join(TRASH_PATH, new_filename)
os.rename(src, destpath)
def find_ext_volume_global_trash(volume_root):
# from [2] Trash directories (1) check for a .Trash dir with the right
# permissions set.
trash_dir = op.join(volume_root, TOPDIR_TRASH)
if not op.exists(trash_dir):
return None
def send2trash(path):
if not isinstance(path, unicode):
path = unicode(path, sys.getfilesystemencoding())
mode = os.lstat(trash_dir).st_mode
# vol/.Trash must be a directory, cannot be a symlink, and must have the
# sticky bit set.
if not op.isdir(trash_dir) or op.islink(trash_dir) or not (mode & stat.S_ISVTX):
return None
trash_dir = op.join(trash_dir, text_type(uid).encode("ascii"))
try:
move_without_conflict(path, TRASH_PATH)
check_create(trash_dir)
except OSError:
# We're probably on an external volume
mount_point = find_mount_point(path)
dest_trash = find_ext_volume_trash(mount_point)
move_without_conflict(path, dest_trash)
return None
return trash_dir
def find_ext_volume_fallback_trash(volume_root):
# from [2] Trash directories (1) create a .Trash-$uid dir.
trash_dir = op.join(volume_root, TOPDIR_FALLBACK)
# Try to make the directory, if we lack permission, raise TrashPermissionError
try:
check_create(trash_dir)
except OSError as e:
if e.errno == errno.EACCES:
raise TrashPermissionError(e.filename)
raise
return trash_dir
def find_ext_volume_trash(volume_root):
trash_dir = find_ext_volume_global_trash(volume_root)
if trash_dir is None:
trash_dir = find_ext_volume_fallback_trash(volume_root)
return trash_dir
# Pull this out so it's easy to stub (to avoid stubbing lstat itself)
def get_dev(path):
return os.lstat(path).st_dev
def send2trash(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)
# ...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)
# 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 path_dev == trash_dev:
topdir = XDG_DATA_HOME
dest_trash = HOMETRASH_B
else:
topdir = find_mount_point(path_b)
trash_dev = get_dev(topdir)
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)

View File

@@ -1,15 +1,20 @@
# Copyright 2010 Hardcoded Software (http://www.hardcoded.net)
# 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
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
import os.path as op
import _send2trash_win
from __future__ import unicode_literals
from platform import version
def send2trash(path):
if not isinstance(path, unicode):
path = unicode(path, 'mbcs')
if not op.isabs(path):
path = op.abspath(path)
_send2trash_win.send(path)
# if windows is vista or newer and pywin32 is available use IFileOperation
if int(version().split(".", 1)[0]) >= 6:
try:
# Attempt to use pywin32 to use IFileOperation
from .plat_win_modern import send2trash
except ImportError:
# use SHFileOperation as fallback
from .plat_win_legacy import send2trash
else:
# use SHFileOperation as fallback
from .plat_win_legacy import send2trash # noqa: F401

View File

@@ -0,0 +1,147 @@
# 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 .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 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)
# 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:
raise WindowsError(result, FormatError(result), paths)

View File

@@ -0,0 +1,72 @@
# 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 .util import preprocess_paths
from platform import version
import pythoncom
import pywintypes
from win32com.shell import shell, shellcon
from .IFileOperationProgressSink import CreateSink
def send2trash(paths):
paths = preprocess_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]
# 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,
)
# default flags to use
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+
)
else:
flags |= shellcon.FOF_ALLOWUNDO
# set the flags
fileop.SetOperationFlags(flags)
# actually try to perform the operation, this section may throw a
# pywintypes.com_error which does not seem to create as nice of an
# error as OSError so wrapping with try to convert
sink = CreateSink()
try:
for path in paths:
item = shell.SHCreateItemFromParsingName(path, None, shell.IID_IShellItem)
fileop.DeleteItem(item, sink)
result = fileop.PerformOperations()
aborted = fileop.GetAnyOperationsAborted()
# if non-zero result or aborted throw an exception
if result or aborted:
raise OSError(None, None, paths, result)
except pywintypes.com_error as error:
# 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()

16
send2trash/util.py Normal file
View File

@@ -0,0 +1,16 @@
# 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
def preprocess_paths(paths):
if 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

@@ -1,33 +1,49 @@
import sys
import os.path as op
from setuptools import setup
from distutils.extension import Extension
exts = []
CLASSIFIERS = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: BSD License",
"Operating System :: MacOS :: MacOS X",
"Operating System :: Microsoft :: Windows",
"Operating System :: POSIX",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Topic :: Desktop Environment :: File Managers",
]
if sys.platform == 'darwin':
exts.append(Extension(
'_send2trash_osx',
[op.join('modules', 'send2trash_osx.c')],
extra_link_args=['-framework', 'CoreServices'],
))
if sys.platform == 'win32':
exts.append(Extension(
'_send2trash_win',
[op.join('modules', 'send2trash_win.c')],
extra_link_args = ['shell32.lib'],
))
with open("README.rst", "rt") as f1, open("CHANGES.rst", "rt") as f2:
LONG_DESCRIPTION = f1.read() + "\n\n" + f2.read()
setup(
name='Send2Trash',
version='1.0.0',
author='Hardcoded Software',
author_email='hsoft@hardcoded.net',
packages=['send2trash'],
name="Send2Trash",
version="1.8.0",
author="Andrew Senetar",
author_email="arsenetar@voltaicideas.net",
packages=["send2trash"],
scripts=[],
ext_modules = exts,
url='http://hg.hardcoded.net/send2trash/',
license='LICENSE',
description='Send file to trash natively under Mac OS X, Windows and Linux.',
)
test_suite="tests",
url="https://github.com/arsenetar/send2trash",
license="BSD License",
description="Send file to trash natively under Mac OS X, Windows and Linux.",
long_description=LONG_DESCRIPTION,
long_description_content_type="text/x-rst",
classifiers=CLASSIFIERS,
extras_require={
"win32": ['pywin32; sys_platform == "win32"'],
"objc": ['pyobjc-framework-Cocoa; sys_platform == "darwin"'],
"nativeLib": [
'pywin32; sys_platform == "win32"',
'pyobjc-framework-Cocoa; sys_platform == "darwin"',
],
},
project_urls={"Bug Reports": "https://github.com/arsenetar/send2trash/issues"},
entry_points={"console_scripts": ["send2trash=send2trash.__main__:main"]},
)

0
tests/__init__.py Normal file
View File

235
tests/test_plat_other.py Normal file
View File

@@ -0,0 +1,235 @@
# encoding: utf-8
import pytest
import codecs
import os
import sys
from os import path as op
from send2trash.compat import PY3
from send2trash import TrashPermissionError
try:
from configparser import ConfigParser
except ImportError:
# py2
from ConfigParser import ConfigParser # noqa: F401
from tempfile import mkdtemp, NamedTemporaryFile, mktemp
import shutil
import stat
if sys.platform != "win32":
import send2trash.plat_other
from send2trash.plat_other import send2trash as s2t
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 + ".trashinfo"))
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 + ".trashinfo"))
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):
with open(path, "a"):
os.utime(path, None)
def _filesys_enc():
enc = sys.getfilesystemencoding()
# Get canonical name of codec
return codecs.lookup(enc).name
@pytest.fixture
def testUnicodefile():
name = u"send2trash_tést1"
file = op.join(op.expanduser(b"~"), name.encode("utf-8"))
touch(file)
assert op.exists(file) is True
yield file
# Cleanup trash files on supported platforms
if sys.platform != "win32":
# Remove trash files if they exist
if op.exists(op.join(HOMETRASH, "files", name)):
os.remove(op.join(HOMETRASH, "files", name))
os.remove(op.join(HOMETRASH, "info", name + ".trashinfo"))
if op.exists(file):
os.remove(file)
@pytest.mark.skipif(_filesys_enc() == "ascii", reason="Requires Unicode filesystem")
def test_trash_bytes(testUnicodefile):
s2t(testUnicodefile)
assert not op.exists(testUnicodefile)
@pytest.mark.skipif(_filesys_enc() == "ascii", reason="Requires Unicode filesystem")
def test_trash_unicode(testUnicodefile):
s2t(testUnicodefile.decode(sys.getfilesystemencoding()))
assert not op.exists(testUnicodefile)
class ExtVol:
def __init__(self, path):
self.trashTopdir = path
if PY3:
self.trashTopdir_b = os.fsencode(self.trashTopdir)
else:
self.trashTopdir_b = self.trashTopdir
def s_getdev(path):
from send2trash.plat_other import is_parent
st = os.lstat(path)
if is_parent(self.trashTopdir, path):
return "dev"
return st.st_dev
def s_ismount(path):
if op.realpath(path) in (
op.realpath(self.trashTopdir),
op.realpath(self.trashTopdir_b),
):
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 cleanup(self):
send2trash.plat_other.get_dev = self.old_getdev
send2trash.plat_other.os.path.ismount = self.old_ismount
shutil.rmtree(self.trashTopdir)
@pytest.fixture
def testExtVol():
trashTopdir = mkdtemp(prefix="s2t")
volume = ExtVol(trashTopdir)
fileName = "test.txt"
filePath = op.join(volume.trashTopdir, fileName)
touch(filePath)
assert op.exists(filePath) is True
yield volume, fileName, filePath
volume.cleanup()
def test_trash_topdir(testExtVol):
trashDir = op.join(testExtVol[0].trashTopdir, ".Trash")
os.mkdir(trashDir, 0o777 | stat.S_ISVTX)
s2t(testExtVol[2])
assert op.exists(testExtVol[2]) is False
assert (
op.exists(op.join(trashDir, str(os.getuid()), "files", testExtVol[1])) is True
)
assert (
op.exists(
op.join(trashDir, str(os.getuid()), "info", testExtVol[1] + ".trashinfo",)
)
is True
)
# info relative path (if another test is added, with the same fileName/Path,
# then it gets renamed etc.)
cfg = ConfigParser()
cfg.read(op.join(trashDir, str(os.getuid()), "info", testExtVol[1] + ".trashinfo"))
assert (testExtVol[1] == cfg.get("Trash Info", "Path", raw=True)) is True
def test_trash_topdir_fallback(testExtVol):
s2t(testExtVol[2])
assert op.exists(testExtVol[2]) is False
assert (
op.exists(
op.join(
testExtVol[0].trashTopdir,
".Trash-" + str(os.getuid()),
"files",
testExtVol[1],
)
)
is True
)
def test_trash_topdir_failure(testExtVol):
os.chmod(testExtVol[0].trashTopdir, 0o500) # not writable to induce the exception
pytest.raises(TrashPermissionError, s2t, [testExtVol[2]])
os.chmod(testExtVol[0].trashTopdir, 0o700) # writable to allow deletion
def test_trash_symlink(testExtVol):
# Use mktemp (race conditioney but no symlink equivalent)
# Since is_parent uses realpath(), and our getdev uses is_parent,
# this should work
slDir = mktemp(prefix="s2t", dir=op.expanduser("~"))
os.mkdir(op.join(testExtVol[0].trashTopdir, "subdir"), 0o700)
filePath = op.join(testExtVol[0].trashTopdir, "subdir", testExtVol[1])
touch(filePath)
os.symlink(op.join(testExtVol[0].trashTopdir, "subdir"), slDir)
s2t(op.join(slDir, testExtVol[1]))
assert op.exists(filePath) is False
assert (
op.exists(
op.join(
testExtVol[0].trashTopdir,
".Trash-" + str(os.getuid()),
"files",
testExtVol[1],
)
)
is True
)
os.remove(slDir)

203
tests/test_plat_win.py Normal file
View File

@@ -0,0 +1,203 @@
# encoding: utf-8
import os
import shutil
import sys
import pytest
from os import path as op
from send2trash import send2trash as s2t
# import the two versions as well as the "automatic" version
if sys.platform == "win32":
from send2trash.plat_win_modern import send2trash as s2t_modern
from send2trash.plat_win_legacy import send2trash as s2t_legacy
else:
pytest.skip("Skipping windows-only tests", allow_module_level=True)
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")
@pytest.fixture
def testdir(tmp_path):
dirname = "\\\\?\\" + str(tmp_path)
assert op.exists(dirname) is True
yield dirname
shutil.rmtree(dirname, ignore_errors=True)
@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
@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(dir, fcn):
fcn(dir)
assert op.exists(dir) is False
def _trash_file(file, fcn):
fcn(file)
assert op.exists(file) is False
def _trash_multifile(files, fcn):
fcn(files)
assert any([op.exists(file) for file in files]) is False
def _file_not_found(dir, fcn):
file = op.join(dir, "otherfile.txt")
pytest.raises(OSError, fcn, file)
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)
shutil.rmtree(dirname, ignore_errors=True)
@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)

43
tests/test_script_main.py Normal file
View File

@@ -0,0 +1,43 @@
# encoding: utf-8
import os
import sys
import pytest
from tempfile import NamedTemporaryFile
from os import path as op
from send2trash.__main__ import main as trash_main
# Only import HOMETRASH on supported platforms
if sys.platform != "win32":
from send2trash.plat_other import HOMETRASH
@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)
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

23
tox.ini Normal file
View File

@@ -0,0 +1,23 @@
[tox]
envlist = py{27,34,35,36,37,38,39,310}
skip_missing_interpreters = True
[testenv]
deps =
flake8
pytest
pywin32; sys_platform == 'win32'
pyobjc-framework-Cocoa; sys_platform == 'darwin'
commands =
flake8
pytest
[testenv:py27]
deps =
configparser
{[testenv]deps}
[flake8]
exclude = .tox,env,build
max-line-length = 120
ignore = E731,E203,E501,W503