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

17 Commits
1.3.0 ... 1.4.0

Author SHA1 Message Date
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
14 changed files with 291 additions and 188 deletions

4
.gitignore vendored
View File

@@ -2,4 +2,6 @@
*.egg-info *.egg-info
/build /build
/dist /dist
.DS_Store .DS_Store
/.tox
__pycache__

10
.travis.yml Normal file
View File

@@ -0,0 +1,10 @@
language: python
python:
- "2.7"
- "3.4"
- "3.5"
- "3.6"
install:
- "pip install tox"
script:
- "tox -e $(echo py$TRAVIS_PYTHON_VERSION | tr -d .)"

View File

@@ -1,11 +1,22 @@
Changes Changes
======= =======
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 Version 1.3.0 -- 2013/07/19
--------------------------- ---------------------------
* Added support for Gnome's GIO. * Added support for Gnome's GIO.
* Merged Python 3 and Python 2 vesion in a single codebase. * Merged Python 3 and Python 2 versions in a single codebase.
Version 1.2.0 -- 2011/03/16 Version 1.2.0 -- 2011/03/16
--------------------------- ---------------------------
@@ -30,4 +41,4 @@ Version 1.0.1 -- 2010/04/19
Version 1.0.0 -- 2010/04/07 Version 1.0.0 -- 2010/04/07
--------------------------- ---------------------------
* Initial Release * Initial Release

View File

@@ -1,4 +1,4 @@
Copyright (c) 2013, Hardcoded Software, http://www.hardcoded.net Copyright (c) 2017, Virgil Dupras
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 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. * 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. * 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.

View File

@@ -1,13 +1,18 @@
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net) # Copyright 2017 Virgil Dupras
# This software is licensed under the "BSD" License as described in the "LICENSE" file, # 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 # which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license # http://www.hardcoded.net/licenses/bsd_license
import sys import sys
if sys.version < '3': import os
text_type = unicode
binary_type = str PY3 = sys.version_info[0] >= 3
else: if PY3:
text_type = str text_type = str
binary_type = bytes binary_type = bytes
environb = os.environb
else:
text_type = unicode
binary_type = str
environb = os.environ

View File

@@ -1,7 +1,7 @@
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net) # Copyright 2017 Virgil Dupras
# This software is licensed under the "BSD" License as described in the "LICENSE" file, # 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 # which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license # http://www.hardcoded.net/licenses/bsd_license
from gi.repository import GObject, Gio from gi.repository import GObject, Gio

View File

@@ -1,7 +1,7 @@
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net) # Copyright 2017 Virgil Dupras
# This software is licensed under the "BSD" License as described in the "LICENSE" file, # 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 # which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license # http://www.hardcoded.net/licenses/bsd_license
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -1,7 +1,7 @@
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net) # Copyright 2017 Virgil Dupras
# This software is licensed under the "BSD" License as described in the "LICENSE" file, # 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 # which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license # http://www.hardcoded.net/licenses/bsd_license
# This is a reimplementation of plat_other.py with reference to the # This is a reimplementation of plat_other.py with reference to the
@@ -27,28 +27,46 @@ except ImportError:
# Python 2 # Python 2
from urllib import quote from urllib import quote
FILES_DIR = 'files' from .compat import text_type, environb
INFO_DIR = 'info'
INFO_SUFFIX = '.trashinfo' try:
fsencode = os.fsencode # Python 3
fsdecode = os.fsdecode
except AttributeError:
def fsencode(u): # Python 2
return u.encode(sys.getfilesystemencoding())
def fsdecode(b):
return b.decode(sys.getfilesystemencoding())
# The Python 3 versions are a bit smarter, handling surrogate escapes,
# but these should work in most cases.
FILES_DIR = b'files'
INFO_DIR = b'info'
INFO_SUFFIX = b'.trashinfo'
# Default of ~/.local/share [3] # Default of ~/.local/share [3]
XDG_DATA_HOME = op.expanduser(os.environ.get('XDG_DATA_HOME', '~/.local/share')) XDG_DATA_HOME = op.expanduser(environb.get(b'XDG_DATA_HOME', b'~/.local/share'))
HOMETRASH = op.join(XDG_DATA_HOME, 'Trash') HOMETRASH_B = op.join(XDG_DATA_HOME, b'Trash')
HOMETRASH = fsdecode(HOMETRASH_B)
uid = os.getuid() uid = os.getuid()
TOPDIR_TRASH = '.Trash' TOPDIR_TRASH = b'.Trash'
TOPDIR_FALLBACK = '.Trash-' + str(uid) TOPDIR_FALLBACK = b'.Trash-' + text_type(uid).encode('ascii')
def is_parent(parent, path): def is_parent(parent, path):
path = op.realpath(path) # In case it's a symlink path = op.realpath(path) # In case it's a symlink
if isinstance(path, text_type):
path = fsencode(path)
parent = op.realpath(parent) parent = op.realpath(parent)
if isinstance(parent, text_type):
parent = fsencode(parent)
return path.startswith(parent) return path.startswith(parent)
def format_date(date): def format_date(date):
return date.strftime("%Y-%m-%dT%H:%M:%S") return date.strftime("%Y-%m-%dT%H:%M:%S")
def info_for(src, topdir): def info_for(src, topdir):
# ...it MUST not include a ".."" directory, and for files not "under" that # ...it MUST not include a ".." directory, and for files not "under" that
# directory, absolute pathnames must be used. [2] # directory, absolute pathnames must be used. [2]
if topdir is None or not is_parent(topdir, src): if topdir is None or not is_parent(topdir, src):
src = op.abspath(src) src = op.abspath(src)
@@ -75,11 +93,11 @@ def trash_move(src, dst, topdir=None):
destname = filename 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 counter += 1
destname = '%s %s%s' % (base_name, counter, ext) destname = base_name + b' ' + text_type(counter).encode('ascii') + ext
check_create(filespath) check_create(filespath)
check_create(infopath) check_create(infopath)
os.rename(src, op.join(filespath, destname)) os.rename(src, op.join(filespath, destname))
f = open(op.join(infopath, destname + INFO_SUFFIX), 'w') f = open(op.join(infopath, destname + INFO_SUFFIX), 'w')
f.write(info_for(src, topdir)) f.write(info_for(src, topdir))
@@ -99,14 +117,14 @@ def find_ext_volume_global_trash(volume_root):
trash_dir = op.join(volume_root, TOPDIR_TRASH) trash_dir = op.join(volume_root, TOPDIR_TRASH)
if not op.exists(trash_dir): if not op.exists(trash_dir):
return None return None
mode = os.lstat(trash_dir).st_mode mode = os.lstat(trash_dir).st_mode
# vol/.Trash must be a directory, cannot be a symlink, and must have the # vol/.Trash must be a directory, cannot be a symlink, and must have the
# sticky bit set. # sticky bit set.
if not op.isdir(trash_dir) or op.islink(trash_dir) or not (mode & stat.S_ISVTX): if not op.isdir(trash_dir) or op.islink(trash_dir) or not (mode & stat.S_ISVTX):
return None return None
trash_dir = op.join(trash_dir, str(uid)) trash_dir = op.join(trash_dir, text_type(uid).encode('ascii'))
try: try:
check_create(trash_dir) check_create(trash_dir)
except OSError: except OSError:
@@ -132,29 +150,37 @@ def get_dev(path):
return os.lstat(path).st_dev return os.lstat(path).st_dev
def send2trash(path): def send2trash(path):
if not isinstance(path, str): if isinstance(path, text_type):
path = str(path, sys.getfilesystemencoding()) path_b = fsencode(path)
if not op.exists(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("File not found: %s" % path)
# ...should check whether the user has the necessary permissions to delete # ...should check whether the user has the necessary permissions to delete
# it, before starting the trashing operation itself. [2] # it, before starting the trashing operation itself. [2]
if not os.access(path, os.W_OK): if not os.access(path_b, os.W_OK):
raise OSError("Permission denied: %s" % path) raise OSError("Permission denied: %s" % path)
# if the file to be trashed is on the same device as HOMETRASH we # if the file to be trashed is on the same device as HOMETRASH we
# want to move it there. # want to move it there.
path_dev = get_dev(path) path_dev = get_dev(path_b)
# If XDG_DATA_HOME or HOMETRASH do not yet exist we need to stat the # If XDG_DATA_HOME or HOMETRASH do not yet exist we need to stat the
# home directory, and these paths will be created further on if needed. # home directory, and these paths will be created further on if needed.
trash_dev = get_dev(op.expanduser('~')) trash_dev = get_dev(op.expanduser(b'~'))
if path_dev == trash_dev: if path_dev == trash_dev:
topdir = XDG_DATA_HOME topdir = XDG_DATA_HOME
dest_trash = HOMETRASH dest_trash = HOMETRASH_B
else: else:
topdir = find_mount_point(path) topdir = find_mount_point(path_b)
trash_dev = get_dev(topdir) trash_dev = get_dev(topdir)
if trash_dev != path_dev: if trash_dev != path_dev:
raise OSError("Couldn't find mount point for %s" % path) raise OSError("Couldn't find mount point for %s" % path)
dest_trash = find_ext_volume_trash(topdir) dest_trash = find_ext_volume_trash(topdir)
trash_move(path, dest_trash, topdir) trash_move(path_b, dest_trash, topdir)

View File

@@ -1,7 +1,7 @@
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net) # Copyright 2017 Virgil Dupras
# This software is licensed under the "BSD" License as described in the "LICENSE" file, # 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 # which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license # http://www.hardcoded.net/licenses/bsd_license
from __future__ import unicode_literals from __future__ import unicode_literals
@@ -54,6 +54,4 @@ def send2trash(path):
fileop.lpszProgressTitle = None fileop.lpszProgressTitle = None
result = SHFileOperationW(byref(fileop)) result = SHFileOperationW(byref(fileop))
if result: if result:
msg = "Couldn't perform operation. Error code: %d" % result raise WindowsError(None, None, path, result)
raise OSError(msg)

View File

@@ -1,6 +1,3 @@
import sys
import os.path as op
from setuptools import setup from setuptools import setup
CLASSIFIERS = [ CLASSIFIERS = [
@@ -12,6 +9,9 @@ CLASSIFIERS = [
'Operating System :: POSIX', 'Operating System :: POSIX',
'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Topic :: Desktop Environment :: File Managers', 'Topic :: Desktop Environment :: File Managers',
] ]
@@ -19,14 +19,15 @@ LONG_DESCRIPTION = open('README.rst', 'rt').read() + '\n\n' + open('CHANGES.rst'
setup( setup(
name='Send2Trash', name='Send2Trash',
version='1.3.0', version='1.4.0',
author='Hardcoded Software', author='Virgil Dupras',
author_email='hsoft@hardcoded.net', author_email='hsoft@hardcoded.net',
packages=['send2trash'], packages=['send2trash'],
scripts=[], scripts=[],
url='http://github.com/hsoft/send2trash', test_suite='tests',
url='https://github.com/hsoft/send2trash',
license='BSD License', license='BSD License',
description='Send file to trash natively under Mac OS X, Windows and Linux.', description='Send file to trash natively under Mac OS X, Windows and Linux.',
long_description=LONG_DESCRIPTION, long_description=LONG_DESCRIPTION,
classifiers=CLASSIFIERS, classifiers=CLASSIFIERS,
) )

View File

@@ -1,131 +0,0 @@
import unittest
import os
from os import path as op
import send2trash.plat_other
from send2trash.plat_other import send2trash as s2t
from configparser import ConfigParser
from tempfile import mkdtemp, NamedTemporaryFile, mktemp
import shutil
import stat
# Could still use cleaning up. But no longer relies on ramfs.
def touch(path):
with open(path, 'a'):
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):
hometrash = send2trash.plat_other.HOMETRASH
name = op.basename(self.file.name)
os.remove(op.join(hometrash, 'files', name))
os.remove(op.join(hometrash, 'info', name+'.trashinfo'))
#
# 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')
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
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
def s_ismount(path):
if op.realpath(path) == op.realpath(self.trashTopdir):
return True
return old_ismount(path)
send2trash.plat_other.os.path.ismount = s_ismount
send2trash.plat_other.get_dev = s_getdev
def tearDown(self):
send2trash.plat_other.get_dev = self.old_getdev
send2trash.plat_other.os.path.ismount = self.old_ismount
shutil.rmtree(self.trashTopdir)
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)
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(op.join(self.trashDir, str(os.getuid()), 'info', self.fileName + '.trashinfo'))
self.assertEqual(self.fileName, cfg.get('Trash Info', 'Path', 1))
# 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)))
# 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)
# 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 tearDown(self):
os.remove(self.slDir)
TestExtVol.tearDown(self)
if __name__ == '__main__':
unittest.main()

0
tests/__init__.py Normal file
View File

170
tests/test_plat_other.py Normal file
View File

@@ -0,0 +1,170 @@
# encoding: utf-8
import codecs
import unittest
import os
from os import path as op
import send2trash.plat_other
from send2trash.plat_other import send2trash as s2t
from send2trash.compat import PY3
from configparser import ConfigParser
from tempfile import mkdtemp, NamedTemporaryFile, mktemp
import shutil
import stat
import sys
# Could still use cleaning up. But no longer relies on ramfs.
HOMETRASH = send2trash.plat_other.HOMETRASH
def touch(path):
with open(path, 'a'):
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'))
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'))
#
# 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')
if PY3:
trashTopdir_b = os.fsencode(self.trashTopdir)
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
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(trashTopdir_b)):
return True
return old_ismount(path)
send2trash.plat_other.os.path.ismount = s_ismount
send2trash.plat_other.get_dev = s_getdev
def tearDown(self):
send2trash.plat_other.get_dev = self.old_getdev
send2trash.plat_other.os.path.ismount = self.old_ismount
shutil.rmtree(self.trashTopdir)
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)
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(op.join(self.trashDir, str(os.getuid()), 'info', self.fileName + '.trashinfo'))
self.assertEqual(self.fileName, cfg.get('Trash Info', 'Path', raw=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)))
# 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)
# 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 tearDown(self):
os.remove(self.slDir)
TestExtVol.tearDown(self)
if __name__ == '__main__':
unittest.main()

11
tox.ini Normal file
View File

@@ -0,0 +1,11 @@
[tox]
envlist = py27,py34,py35,py36
skip_missing_interpreters = True
[testenv]
commands =
python setup.py test
[testenv:py27]
deps =
configparser