1
0
mirror of https://github.com/arsenetar/send2trash.git synced 2025-08-31 04:59:41 +00:00

Compare commits

..

No commits in common. "10c7693d113220781b6c77f512eca69df128b935" and "5d3835735ec10f77b660b664827775f95df76c56" have entirely different histories.

11 changed files with 377 additions and 510 deletions

View File

@ -3,14 +3,16 @@ matrix:
include: include:
- os: windows - os: windows
language: sh language: sh
python: "3.8" python: 3
env: "PATH=/c/Python38:/c/Python38/Scripts:$PATH"
# Perform the manual steps on windows to install python3 # Perform the manual steps on windows to install python3
before_install: before_install:
- choco install python --version=3.8.6 - choco install python3 --params "/InstallDir:C:\Python"
- export PATH="/c/Python:/c/Python/Scripts:$PATH"
- python -m pip install --upgrade pip - python -m pip install --upgrade pip
- python -m pip install pywin32
before_script: before_script:
- export TOXENV=py38 - export TOXENV=py3-win
- python: "2.7" - python: "2.7"
- python: "3.4" - python: "3.4"
- python: "3.5" - python: "3.5"
@ -25,6 +27,7 @@ matrix:
arch: ppc64le arch: ppc64le
- python: "3.6" - python: "3.6"
arch: ppc64le arch: ppc64le
install: install:
- python -m pip install tox - python -m pip install tox
before_script: before_script:

View File

@ -1,105 +0,0 @@
# 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

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

View File

@ -110,7 +110,7 @@ def trash_move(src, dst, topdir=None):
check_create(filespath) check_create(filespath)
check_create(infopath) check_create(infopath)
with open(op.join(infopath, destname + INFO_SUFFIX), "w") as f: with open(op.join(infopath, destname + INFO_SUFFIX), "w") as f:
f.write(info_for(src, topdir)) f.write(info_for(src, topdir))
os.rename(src, op.join(filespath, destname)) os.rename(src, op.join(filespath, destname))

View File

@ -77,6 +77,8 @@ def send2trash(paths):
paths = [op.abspath(path) if not op.isabs(path) else path for path in paths] paths = [op.abspath(path) if not op.isabs(path) else path for path in paths]
# get short path to handle path length issues # get short path to handle path length issues
paths = [get_short_path_name(path) for path in paths] paths = [get_short_path_name(path) for path in paths]
# convert to a single string of null terminated paths
paths = "\0".join(paths)
fileop = SHFILEOPSTRUCTW() fileop = SHFILEOPSTRUCTW()
fileop.hwnd = 0 fileop.hwnd = 0
fileop.wFunc = FO_DELETE fileop.wFunc = FO_DELETE
@ -91,15 +93,7 @@ def send2trash(paths):
# NOTE: based on how python allocates memory for these types they should # 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 # 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'. # setting the last two characters to null using buffer[index] = '\0'.
# Additional note on another issue here, unicode_buffer expects length in buffer = create_unicode_buffer(paths, len(paths) + 2)
# 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.pFrom = LPCWSTR(addressof(buffer))
fileop.pTo = None fileop.pTo = None
fileop.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT fileop.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT

View File

@ -11,7 +11,6 @@ from platform import version
import pythoncom import pythoncom
import pywintypes import pywintypes
from win32com.shell import shell, shellcon from win32com.shell import shell, shellcon
from .IFileOperationProgressSink import CreateSink
def send2trash(paths): def send2trash(paths):
@ -51,11 +50,10 @@ def send2trash(paths):
# actually try to perform the operation, this section may throw a # actually try to perform the operation, this section may throw a
# pywintypes.com_error which does not seem to create as nice of an # pywintypes.com_error which does not seem to create as nice of an
# error as OSError so wrapping with try to convert # error as OSError so wrapping with try to convert
sink = CreateSink()
try: try:
for path in paths: for path in paths:
item = shell.SHCreateItemFromParsingName(path, None, shell.IID_IShellItem) item = shell.SHCreateItemFromParsingName(path, None, shell.IID_IShellItem)
fileop.DeleteItem(item, sink) fileop.DeleteItem(item)
result = fileop.PerformOperations() result = fileop.PerformOperations()
aborted = fileop.GetAnyOperationsAborted() aborted = fileop.GetAnyOperationsAborted()
# if non-zero result or aborted throw an exception # if non-zero result or aborted throw an exception

View File

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

View File

@ -1,82 +1,25 @@
# encoding: utf-8 # encoding: utf-8
import pytest
import codecs import codecs
import unittest
import os import os
import sys
from os import path as op from os import path as op
import send2trash.plat_other
from send2trash.plat_other import send2trash as s2t
from send2trash.compat import PY3 from send2trash.compat import PY3
from send2trash import TrashPermissionError
try: try:
from configparser import ConfigParser from configparser import ConfigParser
except ImportError: except ImportError:
# py2 # py2
from ConfigParser import ConfigParser # noqa: F401 from ConfigParser import ConfigParser
from tempfile import mkdtemp, NamedTemporaryFile, mktemp from tempfile import mkdtemp, NamedTemporaryFile, mktemp
import shutil import shutil
import stat import stat
import sys
if sys.platform != "win32": # Could still use cleaning up. But no longer relies on ramfs.
import send2trash.plat_other
from send2trash.plat_other import send2trash as s2t
HOMETRASH = send2trash.plat_other.HOMETRASH 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): def touch(path):
@ -84,48 +27,100 @@ def touch(path):
os.utime(path, None) os.utime(path, None)
class TestHomeTrash(unittest.TestCase):
def setUp(self):
self.file = NamedTemporaryFile(
dir=op.expanduser("~"), prefix="send2trash_test", delete=False
)
def test_trash(self):
s2t(self.file.name)
self.assertFalse(op.exists(self.file.name))
def tearDown(self):
name = op.basename(self.file.name)
os.remove(op.join(HOMETRASH, "files", name))
os.remove(op.join(HOMETRASH, "info", name + ".trashinfo"))
class TestHomeMultiTrash(unittest.TestCase):
def setUp(self):
self.files = list(
map(
lambda index: NamedTemporaryFile(
dir=op.expanduser("~"),
prefix="send2trash_test{}".format(index),
delete=False,
),
range(10),
)
)
def test_multitrash(self):
filenames = [file.name for file in self.files]
s2t(filenames)
self.assertFalse(any([op.exists(filename) for filename in filenames]))
def tearDown(self):
filenames = [op.basename(file.name) for file in self.files]
[os.remove(op.join(HOMETRASH, "files", filename)) for filename in filenames]
[
os.remove(op.join(HOMETRASH, "info", filename + ".trashinfo"))
for filename in filenames
]
def _filesys_enc(): def _filesys_enc():
enc = sys.getfilesystemencoding() enc = sys.getfilesystemencoding()
# Get canonical name of codec # Get canonical name of codec
return codecs.lookup(enc).name return codecs.lookup(enc).name
@pytest.fixture @unittest.skipIf(_filesys_enc() == "ascii", "ASCII filesystem")
def testUnicodefile(): class TestUnicodeTrash(unittest.TestCase):
name = u"send2trash_tést1" def setUp(self):
file = op.join(op.expanduser(b"~"), name.encode("utf-8")) self.name = u"send2trash_tést1"
touch(file) self.file = op.join(op.expanduser(b"~"), self.name.encode("utf-8"))
assert op.exists(file) is True touch(self.file)
yield file
# Cleanup trash files on supported platforms def test_trash_bytes(self):
if sys.platform != "win32": s2t(self.file)
# Remove trash files if they exist assert not op.exists(self.file)
if op.exists(op.join(HOMETRASH, "files", name)):
os.remove(op.join(HOMETRASH, "files", name)) def test_trash_unicode(self):
os.remove(op.join(HOMETRASH, "info", name + ".trashinfo")) s2t(self.file.decode(sys.getfilesystemencoding()))
if op.exists(file): assert not op.exists(self.file)
os.remove(file)
def tearDown(self):
if op.exists(self.file):
os.remove(self.file)
trash_file = op.join(HOMETRASH, "files", self.name)
if op.exists(trash_file):
os.remove(trash_file)
os.remove(op.join(HOMETRASH, "info", self.name + ".trashinfo"))
@pytest.mark.skipif(_filesys_enc() == "ascii", reason="Requires Unicode filesystem") #
def test_trash_bytes(testUnicodefile): # Tests for files on some other volume than the user's home directory.
s2t(testUnicodefile) #
assert not op.exists(testUnicodefile) # 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)
@pytest.mark.skipif(_filesys_enc() == "ascii", reason="Requires Unicode filesystem") #
def test_trash_unicode(testUnicodefile): class TestExtVol(unittest.TestCase):
s2t(testUnicodefile.decode(sys.getfilesystemencoding())) def setUp(self):
assert not op.exists(testUnicodefile) self.trashTopdir = mkdtemp(prefix="s2t")
class ExtVol:
def __init__(self, path):
self.trashTopdir = path
if PY3: if PY3:
self.trashTopdir_b = os.fsencode(self.trashTopdir) trashTopdir_b = os.fsencode(self.trashTopdir)
else: else:
self.trashTopdir_b = self.trashTopdir 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): def s_getdev(path):
from send2trash.plat_other import is_parent from send2trash.plat_other import is_parent
@ -138,98 +133,120 @@ class ExtVol:
def s_ismount(path): def s_ismount(path):
if op.realpath(path) in ( if op.realpath(path) in (
op.realpath(self.trashTopdir), op.realpath(self.trashTopdir),
op.realpath(self.trashTopdir_b), op.realpath(trashTopdir_b),
): ):
return True return True
return old_ismount(path) 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.os.path.ismount = s_ismount
send2trash.plat_other.get_dev = s_getdev send2trash.plat_other.get_dev = s_getdev
def cleanup(self): def tearDown(self):
send2trash.plat_other.get_dev = self.old_getdev send2trash.plat_other.get_dev = self.old_getdev
send2trash.plat_other.os.path.ismount = self.old_ismount send2trash.plat_other.os.path.ismount = self.old_ismount
shutil.rmtree(self.trashTopdir) shutil.rmtree(self.trashTopdir)
@pytest.fixture class TestTopdirTrash(TestExtVol):
def testExtVol(): def setUp(self):
trashTopdir = mkdtemp(prefix="s2t") TestExtVol.setUp(self)
volume = ExtVol(trashTopdir) # Create a .Trash dir w/ a sticky bit
fileName = "test.txt" self.trashDir = op.join(self.trashTopdir, ".Trash")
filePath = op.join(volume.trashTopdir, fileName) os.mkdir(self.trashDir, 0o777 | stat.S_ISVTX)
touch(filePath)
assert op.exists(filePath) is True
yield volume, fileName, filePath
volume.cleanup()
def test_trash(self):
def test_trash_topdir(testExtVol): s2t(self.filePath)
trashDir = op.join(testExtVol[0].trashTopdir, ".Trash") self.assertFalse(op.exists(self.filePath))
os.mkdir(trashDir, 0o777 | stat.S_ISVTX) self.assertTrue(
op.exists(op.join(self.trashDir, str(os.getuid()), "files", self.fileName))
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 self.assertTrue(
) op.exists(
# info relative path (if another test is added, with the same fileName/Path, op.join(
# then it gets renamed etc.) self.trashDir,
cfg = ConfigParser() str(os.getuid()),
cfg.read(op.join(trashDir, str(os.getuid()), "info", testExtVol[1] + ".trashinfo")) "info",
assert (testExtVol[1] == cfg.get("Trash Info", "Path", raw=True)) is True self.fileName + ".trashinfo",
)
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 # info relative path (if another test is added, with the same fileName/Path,
) # then it gets renamed etc.)
cfg = ConfigParser()
cfg.read(
def test_trash_topdir_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( op.join(
testExtVol[0].trashTopdir, self.trashDir, str(os.getuid()), "info", self.fileName + ".trashinfo"
".Trash-" + str(os.getuid()),
"files",
testExtVol[1],
) )
) )
is True self.assertEqual(self.fileName, cfg.get("Trash Info", "Path", raw=True))
)
os.remove(slDir)
# 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()

View File

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

View File

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

17
tox.ini
View File

@ -1,20 +1,21 @@
[tox] [tox]
envlist = py{27,34,35,36,37,38,39,310} envlist = py{27,34,35,36,37,38,39,310,3-win}
skip_missing_interpreters = True skip_missing_interpreters = True
[testenv] [testenv]
deps = platform = linux
flake8
pytest
pywin32; sys_platform == 'win32'
commands = commands =
flake8 python setup.py test --test-suite tests.TestSuite
pytest
[testenv:py3-win]
platform = win
commands =
python -m pip install pywin32
python setup.py test --test-suite tests.TestSuite
[testenv:py27] [testenv:py27]
deps = deps =
configparser configparser
{[testenv]deps}
[flake8] [flake8]
exclude = .tox,env,build exclude = .tox,env,build