Skip to content

Commit 0207903

Browse files
DeathAxedeathaxe
authored andcommitted
Add support for python 3.8 and Sublime Text 4
Sublime Text 4 comes with a new plugin_host based on python 3.8. This commit adds an `.python-version` file to opt-in to python 3.8 and updates the `send2trash` library for compatibility reasons with the new interpreter.
1 parent 5bab357 commit 0207903

8 files changed

Lines changed: 193 additions & 82 deletions

File tree

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.8

libs/send2trash/LICENSE

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Copyright (c) 2017, Virgil Dupras
2+
All rights reserved.
3+
4+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5+
6+
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7+
* 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.
8+
* 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.
9+
10+
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.

libs/send2trash/__init__.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
1-
# Copyright 2010 Hardcoded Software (http://www.hardcoded.net)
1+
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
22

3-
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
4-
# which should be included with this package. The terms are also available at
3+
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
4+
# which should be included with this package. The terms are also available at
55
# http://www.hardcoded.net/licenses/bsd_license
66

77
import sys
88

9-
if sys.platform == "darwin":
9+
from .exceptions import TrashPermissionError
10+
11+
if sys.platform == 'darwin':
1012
from .plat_osx import send2trash
11-
elif sys.platform == "win32":
13+
elif sys.platform == 'win32':
1214
from .plat_win import send2trash
1315
else:
14-
from .plat_other import send2trash
16+
try:
17+
# If we can use gio, let's use it
18+
from .plat_gio import send2trash
19+
except ImportError:
20+
# Oh well, let's fallback to our own Freedesktop trash implementation
21+
from .plat_other import send2trash

libs/send2trash/compat.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Copyright 2017 Virgil Dupras
2+
3+
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
4+
# which should be included with this package. The terms are also available at
5+
# http://www.hardcoded.net/licenses/bsd_license
6+
7+
import sys
8+
import os
9+
10+
PY3 = sys.version_info[0] >= 3
11+
if PY3:
12+
text_type = str
13+
binary_type = bytes
14+
if os.supports_bytes_environ:
15+
# environb will be unset under Windows, but then again we're not supposed to use it.
16+
environb = os.environb
17+
else:
18+
text_type = unicode
19+
binary_type = str
20+
environb = os.environ

libs/send2trash/exceptions.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import errno
2+
from .compat import PY3
3+
4+
if PY3:
5+
_permission_error = PermissionError
6+
else:
7+
_permission_error = OSError
8+
9+
class TrashPermissionError(_permission_error):
10+
"""A permission error specific to a trash directory.
11+
12+
Raising this error indicates that permissions prevent us efficiently
13+
trashing a file, although we might still have permission to delete it.
14+
This is *not* used when permissions prevent removing the file itself:
15+
that will be raised as a regular PermissionError (OSError on Python 2).
16+
17+
Application code that catches this may try to simply delete the file,
18+
or prompt the user to decide, or (on Freedesktop platforms), move it to
19+
'home trash' as a fallback. This last option probably involves copying the
20+
data between partitions, devices, or network drives, so we don't do it as
21+
a fallback.
22+
"""
23+
def __init__(self, filename):
24+
_permission_error.__init__(self, errno.EACCES, "Permission denied",
25+
filename)

libs/send2trash/plat_osx.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
1-
# Copyright 2010 Hardcoded Software (http://www.hardcoded.net)
1+
# Copyright 2017 Virgil Dupras
22

33
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
44
# which should be included with this package. The terms are also available at
55
# http://www.hardcoded.net/licenses/bsd_license
66

7+
from __future__ import unicode_literals
8+
79
from ctypes import cdll, byref, Structure, c_char, c_char_p
810
from ctypes.util import find_library
911

10-
Foundation = cdll.LoadLibrary(find_library("Foundation"))
11-
CoreServices = cdll.LoadLibrary(find_library("CoreServices"))
12+
from .compat import binary_type
13+
14+
Foundation = cdll.LoadLibrary(find_library('Foundation'))
15+
CoreServices = cdll.LoadLibrary(find_library('CoreServices'))
1216

1317
GetMacOSStatusCommentString = Foundation.GetMacOSStatusCommentString
1418
GetMacOSStatusCommentString.restype = c_char_p
@@ -24,20 +28,17 @@
2428
kFSFileOperationDoNotMoveAcrossVolumes = 0x04
2529
kFSFileOperationSkipPreflight = 0x08
2630

27-
2831
class FSRef(Structure):
29-
_fields_ = [("hidden", c_char * 80)]
30-
32+
_fields_ = [('hidden', c_char * 80)]
3133

3234
def check_op_result(op_result):
3335
if op_result:
34-
msg = GetMacOSStatusCommentString(op_result).decode("utf-8")
36+
msg = GetMacOSStatusCommentString(op_result).decode('utf-8')
3537
raise OSError(msg)
3638

37-
3839
def send2trash(path):
39-
if not isinstance(path, bytes):
40-
path = path.encode("utf-8")
40+
if not isinstance(path, binary_type):
41+
path = path.encode('utf-8')
4142
fp = FSRef()
4243
opts = kFSPathMakeRefDoNotFollowLeafSymlink
4344
op_result = FSPathMakeRefWithOptions(path, opts, byref(fp), None)

libs/send2trash/plat_other.py

Lines changed: 74 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2010 Hardcoded Software (http://www.hardcoded.net)
1+
# Copyright 2017 Virgil Dupras
22

33
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
44
# which should be included with this package. The terms are also available at
@@ -14,57 +14,77 @@
1414
# For external volumes this implementation will raise an exception if it can't
1515
# find or create the user's trash directory.
1616

17-
# import sys
17+
from __future__ import unicode_literals
18+
19+
import errno
20+
import sys
1821
import os
1922
import os.path as op
2023
from datetime import datetime
2124
import stat
22-
import shutil
23-
from urllib.parse import quote
24-
25-
FILES_DIR = "files"
26-
INFO_DIR = "info"
27-
INFO_SUFFIX = ".trashinfo"
25+
try:
26+
from urllib.parse import quote
27+
except ImportError:
28+
# Python 2
29+
from urllib import quote
30+
31+
from .compat import text_type, environb
32+
from .exceptions import TrashPermissionError
33+
34+
try:
35+
fsencode = os.fsencode # Python 3
36+
fsdecode = os.fsdecode
37+
except AttributeError:
38+
def fsencode(u): # Python 2
39+
return u.encode(sys.getfilesystemencoding())
40+
def fsdecode(b):
41+
return b.decode(sys.getfilesystemencoding())
42+
# The Python 3 versions are a bit smarter, handling surrogate escapes,
43+
# but these should work in most cases.
44+
45+
FILES_DIR = b'files'
46+
INFO_DIR = b'info'
47+
INFO_SUFFIX = b'.trashinfo'
2848

2949
# Default of ~/.local/share [3]
30-
XDG_DATA_HOME = op.expanduser(os.environ.get("XDG_DATA_HOME", "~/.local/share"))
31-
HOMETRASH = op.join(XDG_DATA_HOME, "Trash")
50+
XDG_DATA_HOME = op.expanduser(environb.get(b'XDG_DATA_HOME', b'~/.local/share'))
51+
HOMETRASH_B = op.join(XDG_DATA_HOME, b'Trash')
52+
HOMETRASH = fsdecode(HOMETRASH_B)
3253

3354
uid = os.getuid()
34-
TOPDIR_TRASH = ".Trash"
35-
TOPDIR_FALLBACK = ".Trash-" + str(uid)
36-
55+
TOPDIR_TRASH = b'.Trash'
56+
TOPDIR_FALLBACK = b'.Trash-' + text_type(uid).encode('ascii')
3757

3858
def is_parent(parent, path):
39-
path = op.realpath(path) # In case it's a symlink
59+
path = op.realpath(path) # In case it's a symlink
60+
if isinstance(path, text_type):
61+
path = fsencode(path)
4062
parent = op.realpath(parent)
63+
if isinstance(parent, text_type):
64+
parent = fsencode(parent)
4165
return path.startswith(parent)
4266

43-
4467
def format_date(date):
4568
return date.strftime("%Y-%m-%dT%H:%M:%S")
4669

47-
4870
def info_for(src, topdir):
49-
# ...it MUST not include a ".."" directory, and for files not "under" that
71+
# ...it MUST not include a ".." directory, and for files not "under" that
5072
# directory, absolute pathnames must be used. [2]
5173
if topdir is None or not is_parent(topdir, src):
5274
src = op.abspath(src)
5375
else:
5476
src = op.relpath(src, topdir)
5577

56-
info = "[Trash Info]\n"
78+
info = "[Trash Info]\n"
5779
info += "Path=" + quote(src) + "\n"
5880
info += "DeletionDate=" + format_date(datetime.now()) + "\n"
5981
return info
6082

61-
6283
def check_create(dir):
6384
# use 0700 for paths [3]
6485
if not op.exists(dir):
6586
os.makedirs(dir, 0o700)
6687

67-
6888
def trash_move(src, dst, topdir=None):
6989
filename = op.basename(src)
7090
filespath = op.join(dst, FILES_DIR)
@@ -73,32 +93,26 @@ def trash_move(src, dst, topdir=None):
7393

7494
counter = 0
7595
destname = filename
76-
while op.exists(op.join(filespath, destname)) or op.exists(
77-
op.join(infopath, destname + INFO_SUFFIX)
78-
):
96+
while op.exists(op.join(filespath, destname)) or op.exists(op.join(infopath, destname + INFO_SUFFIX)):
7997
counter += 1
80-
destname = "%s %s%s" % (base_name, counter, ext)
98+
destname = base_name + b' ' + text_type(counter).encode('ascii') + ext
8199

82100
check_create(filespath)
83101
check_create(infopath)
84-
try:
85-
os.rename(src, op.join(filespath, destname))
86-
except:
87-
shutil.move(src, op.join(filespath, destname))
88-
f = open(op.join(infopath, destname + INFO_SUFFIX), "w")
102+
103+
os.rename(src, op.join(filespath, destname))
104+
f = open(op.join(infopath, destname + INFO_SUFFIX), 'w')
89105
f.write(info_for(src, topdir))
90106
f.close()
91107

92-
93108
def find_mount_point(path):
94109
# Even if something's wrong, "/" is a mount point, so the loop will exit.
95110
# Use realpath in case it's a symlink
96-
path = op.realpath(path) # Required to avoid infinite loop
111+
path = op.realpath(path) # Required to avoid infinite loop
97112
while not op.ismount(path):
98113
path = op.split(path)[0]
99114
return path
100115

101-
102116
def find_ext_volume_global_trash(volume_root):
103117
# from [2] Trash directories (1) check for a .Trash dir with the right
104118
# permissions set.
@@ -112,61 +126,67 @@ def find_ext_volume_global_trash(volume_root):
112126
if not op.isdir(trash_dir) or op.islink(trash_dir) or not (mode & stat.S_ISVTX):
113127
return None
114128

115-
trash_dir = op.join(trash_dir, str(uid))
129+
trash_dir = op.join(trash_dir, text_type(uid).encode('ascii'))
116130
try:
117131
check_create(trash_dir)
118132
except OSError:
119133
return None
120134
return trash_dir
121135

122-
123136
def find_ext_volume_fallback_trash(volume_root):
124137
# from [2] Trash directories (1) create a .Trash-$uid dir.
125138
trash_dir = op.join(volume_root, TOPDIR_FALLBACK)
126-
# Try to make the directory, if we can't the OSError exception will escape
127-
# be thrown out of send2trash.
128-
check_create(trash_dir)
139+
# Try to make the directory, if we lack permission, raise TrashPermissionError
140+
try:
141+
check_create(trash_dir)
142+
except OSError as e:
143+
if e.errno == errno.EACCES:
144+
raise TrashPermissionError(e.filename)
145+
raise
129146
return trash_dir
130147

131-
132148
def find_ext_volume_trash(volume_root):
133149
trash_dir = find_ext_volume_global_trash(volume_root)
134150
if trash_dir is None:
135151
trash_dir = find_ext_volume_fallback_trash(volume_root)
136152
return trash_dir
137153

138-
139154
# Pull this out so it's easy to stub (to avoid stubbing lstat itself)
140155
def get_dev(path):
141156
return os.lstat(path).st_dev
142157

143-
144158
def send2trash(path):
145-
# if not isinstance(path, str):
146-
# path = str(path, sys.getfilesystemencoding())
147-
# if not op.exists(path):
148-
# raise OSError("File not found: %s" % path)
159+
if isinstance(path, text_type):
160+
path_b = fsencode(path)
161+
elif isinstance(path, bytes):
162+
path_b = path
163+
elif hasattr(path, '__fspath__'):
164+
# Python 3.6 PathLike protocol
165+
return send2trash(path.__fspath__())
166+
else:
167+
raise TypeError('str, bytes or PathLike expected, not %r' % type(path))
168+
169+
if not op.exists(path_b):
170+
raise OSError("File not found: %s" % path)
149171
# ...should check whether the user has the necessary permissions to delete
150172
# it, before starting the trashing operation itself. [2]
151-
# if not os.access(path, os.W_OK):
152-
# raise OSError("Permission denied: %s" % path)
173+
if not os.access(path_b, os.W_OK):
174+
raise OSError("Permission denied: %s" % path)
153175
# if the file to be trashed is on the same device as HOMETRASH we
154176
# want to move it there.
155-
path_dev = get_dev(path)
177+
path_dev = get_dev(path_b)
156178

157179
# If XDG_DATA_HOME or HOMETRASH do not yet exist we need to stat the
158180
# home directory, and these paths will be created further on if needed.
159-
trash_dev = get_dev(op.expanduser("~"))
181+
trash_dev = get_dev(op.expanduser(b'~'))
160182

161-
if path_dev == trash_dev or (
162-
os.path.exists(XDG_DATA_HOME) and os.path.exists(HOMETRASH)
163-
):
183+
if path_dev == trash_dev:
164184
topdir = XDG_DATA_HOME
165-
dest_trash = HOMETRASH
185+
dest_trash = HOMETRASH_B
166186
else:
167-
topdir = find_mount_point(path)
187+
topdir = find_mount_point(path_b)
168188
trash_dev = get_dev(topdir)
169189
if trash_dev != path_dev:
170190
raise OSError("Couldn't find mount point for %s" % path)
171191
dest_trash = find_ext_volume_trash(topdir)
172-
trash_move(path, dest_trash, topdir)
192+
trash_move(path_b, dest_trash, topdir)

0 commit comments

Comments
 (0)