Skip to content

Commit 3ae938f

Browse files
committed
[Diff] upgrade to py3.13
1 parent 9de8e39 commit 3ae938f

File tree

2 files changed

+103
-91
lines changed

2 files changed

+103
-91
lines changed

Diff/.python-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3.8
1+
3.13

Diff/diff.py

Lines changed: 102 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,43 @@
11
import difflib
2-
import os
32
import time
3+
from pathlib import Path
4+
from typing import Iterable, Iterator, List, Optional
45

56
import sublime
67
import sublime_plugin
78

89

9-
def splitlines_keep_ends(text):
10+
def splitlines_keep_ends(text: str) -> List[str]:
11+
"""
12+
Split text into lines but preserve newline endings.
13+
Required for difflib to work correctly.
14+
"""
1015
lines = text.split('\n')
11-
12-
# Need to insert back the newline characters between lines, difflib
13-
# requires this.
14-
if len(lines) > 0:
15-
for i in range(len(lines) - 1):
16-
lines[i] += '\n'
17-
16+
for i in range(len(lines) - 1):
17+
lines[i] += '\n'
1818
return lines
1919

2020

21-
def read_file_lines(fname):
22-
with open(fname, mode='rt', encoding='utf-8') as f:
23-
lines = splitlines_keep_ends(f.read())
24-
25-
# as `difflib` doesn't work properly when the file does not end
26-
# with a new line character (https://bugs.python.org/issue2142),
27-
# we add a warning ourselves to fix it
21+
def read_file_lines(fname: str | Path) -> List[str]:
22+
"""Read a UTF-8 file and return its lines with newline endings preserved."""
23+
path = Path(fname)
24+
text = path.read_text(encoding="utf-8")
25+
lines = splitlines_keep_ends(text)
2826
add_no_eol_warning_if_applicable(lines)
29-
3027
return lines
3128

3229

33-
def add_no_eol_warning_if_applicable(lines):
34-
if len(lines) > 0 and lines[-1]:
35-
# note we update the last line rather than adding a new one
36-
# so that the diff will show the warning with the last line
30+
def add_no_eol_warning_if_applicable(lines: List[str]) -> None:
31+
"""
32+
Append a note if file doesn't end with newline.
33+
difflib misbehaves otherwise.
34+
"""
35+
if lines and lines[-1] and not lines[-1].endswith('\n'):
3736
lines[-1] += '\n\\ No newline at end of file\n'
3837

3938

4039
class DiffFilesCommand(sublime_plugin.WindowCommand):
41-
42-
def run(self, files):
40+
def run(self, files: List[str]) -> None:
4341
if len(files) != 2:
4442
return
4543

@@ -50,23 +48,30 @@ def run(self, files):
5048
sublime.status_message("Diff only works with UTF-8 files")
5149
return
5250

53-
adate = time.ctime(os.stat(files[1]).st_mtime)
54-
bdate = time.ctime(os.stat(files[0]).st_mtime)
51+
a_path, b_path = Path(files[1]), Path(files[0])
52+
adate = time.ctime(a_path.stat().st_mtime)
53+
bdate = time.ctime(b_path.stat().st_mtime)
5554

56-
diff = difflib.unified_diff(a, b, files[1], files[0], adate, bdate)
57-
show_diff_output(diff, None, self.window, f"{os.path.basename(files[1])} -> {os.path.basename(files[0])}", 'diff_files', 'diff_files_to_buffer')
55+
diff = difflib.unified_diff(
56+
a, b, files[1], files[0], adate, bdate, lineterm=""
57+
)
58+
show_diff_output(
59+
diff,
60+
None,
61+
self.window,
62+
f"{a_path.name} -> {b_path.name}",
63+
"diff_files",
64+
"diff_files_to_buffer",
65+
)
5866

59-
def is_visible(self, files):
67+
def is_visible(self, files: List[str]) -> bool:
6068
return len(files) == 2
6169

6270

6371
class DiffChangesCommand(sublime_plugin.TextCommand):
64-
65-
def run(self, edit):
66-
72+
def run(self, edit: sublime.Edit) -> None:
6773
fname = self.view.file_name()
68-
69-
if not fname or not os.path.exists(fname):
74+
if not fname or not Path(fname).exists():
7075
sublime.status_message("Unable to diff changes because the file does not exist")
7176
return
7277

@@ -77,24 +82,31 @@ def run(self, edit):
7782
return
7883

7984
b = get_lines_for_view(self.view)
80-
8185
add_no_eol_warning_if_applicable(b)
8286

83-
adate = time.ctime(os.stat(fname).st_mtime)
87+
adate = time.ctime(Path(fname).stat().st_mtime)
8488
bdate = time.ctime()
8589

86-
diff = difflib.unified_diff(a, b, fname, fname, adate, bdate)
87-
name = "Unsaved Changes: " + os.path.basename(fname)
88-
show_diff_output(diff, self.view, self.view.window(), name, 'unsaved_changes', 'diff_changes_to_buffer')
90+
diff = difflib.unified_diff(a, b, fname, fname, adate, bdate, lineterm="")
91+
name = f"Unsaved Changes: {Path(fname).name}"
92+
show_diff_output(diff, self.view, self.view.window(), name, "unsaved_changes", "diff_changes_to_buffer")
8993

90-
def is_enabled(self):
94+
def is_enabled(self) -> bool:
9195
return self.view.is_dirty() and self.view.file_name() is not None
9296

9397

94-
def show_diff_output(diff, view, win, name, panel_name, buffer_setting_name):
95-
difftxt = u"".join(line for line in diff)
98+
def show_diff_output(
99+
diff: Iterable[str],
100+
view: Optional[sublime.View],
101+
win: sublime.Window,
102+
name: str,
103+
panel_name: str,
104+
buffer_setting_name: str,
105+
) -> None:
106+
"""Display the unified diff either in a scratch buffer or an output panel."""
107+
difftxt = "".join(diff)
96108

97-
if difftxt == "":
109+
if not difftxt:
98110
sublime.status_message("No changes")
99111
return
100112

@@ -107,88 +119,88 @@ def show_diff_output(diff, view, win, name, panel_name, buffer_setting_name):
107119
else:
108120
v = win.create_output_panel(panel_name)
109121
if view:
110-
v.settings().set('word_wrap', view.settings().get('word_wrap'))
122+
v.settings().set("word_wrap", view.settings().get("word_wrap"))
111123

112-
v.assign_syntax('Packages/Diff/Diff.sublime-syntax')
113-
v.run_command('append', {'characters': difftxt, 'disable_tab_translation': True})
124+
v.assign_syntax("Packages/Diff/Diff.sublime-syntax")
125+
v.run_command("append", {"characters": difftxt, "disable_tab_translation": True})
114126

115127
if not use_buffer:
116-
win.run_command('show_panel', {'panel': f'output.{panel_name}'})
128+
win.run_command("show_panel", {"panel": f"output.{panel_name}"})
117129

118130

119-
def get_view_from_tab_context(active_view, **kwargs):
120-
view = active_view
121-
if 'group' in kwargs and 'index' in kwargs:
122-
view = view.window().views_in_group(kwargs['group'])[kwargs['index']]
123-
return view
131+
def get_view_from_tab_context(active_view: sublime.View, **kwargs) -> sublime.View:
132+
"""Return the view associated with the clicked tab."""
133+
if "group" in kwargs and "index" in kwargs:
134+
return active_view.window().views_in_group(kwargs["group"])[kwargs["index"]]
135+
return active_view
124136

125137

126-
def get_views_from_tab_context(active_view, **kwargs):
138+
def get_views_from_tab_context(active_view: sublime.View, **kwargs) -> List[sublime.View]:
139+
"""Return selected views, preserving right-click order."""
127140
selected_views = list(get_selected_views(active_view.window()))
128-
if 'group' in kwargs and 'index' in kwargs:
129-
tab_context_view = get_view_from_tab_context(active_view, **kwargs)
130-
# if the tab which was right clicked on is selected, exclude it from the selected views and re-add it afterwards
131-
# so that the order of the diff will be determined by which tab was right-clicked on
132-
return [view for view in selected_views if view.id() != tab_context_view.id()] + [tab_context_view]
141+
if "group" in kwargs and "index" in kwargs:
142+
tab_view = get_view_from_tab_context(active_view, **kwargs)
143+
return [v for v in selected_views if v.id() != tab_view.id()] + [tab_view]
133144
return selected_views
134145

135146

136-
def get_selected_views(window):
137-
return filter(lambda view: view, map(lambda sheet: sheet.view(), window.selected_sheets()))
147+
def get_selected_views(window: sublime.Window) -> Iterator[sublime.View]:
148+
"""Yield selected views from the given window."""
149+
return filter(None, (sheet.view() for sheet in window.selected_sheets()))
138150

139151

140-
def get_name_for_view(view):
141-
return view.file_name() or view.name() or "Unsaved view ({})".format(view.id())
152+
def get_name_for_view(view: sublime.View) -> str:
153+
return view.file_name() or view.name() or f"Unsaved view ({view.id()})"
142154

143155

144-
def get_lines_for_view(view):
156+
def get_lines_for_view(view: sublime.View) -> List[str]:
157+
"""Return the full text of a view split into lines."""
145158
return splitlines_keep_ends(view.substr(sublime.Region(0, view.size())))
146159

147160

148161
class DiffViewsCommand(sublime_plugin.TextCommand):
149-
def run(self, edit, **kwargs):
162+
def run(self, edit: sublime.Edit, **kwargs) -> None:
150163
views = get_views_from_tab_context(self.view, **kwargs)
151164
if len(views) != 2:
152165
return
153166

154-
view_names = (
155-
get_name_for_view(views[0]),
156-
get_name_for_view(views[1])
157-
)
158-
159-
from_lines = get_lines_for_view(views[0])
160-
to_lines = get_lines_for_view(views[1])
167+
view_names = [get_name_for_view(v) for v in views]
168+
from_lines, to_lines = map(get_lines_for_view, views)
161169
add_no_eol_warning_if_applicable(from_lines)
162170
add_no_eol_warning_if_applicable(to_lines)
163171

164172
diff = difflib.unified_diff(
165173
from_lines,
166174
to_lines,
167175
fromfile=view_names[0],
168-
tofile=view_names[1]
176+
tofile=view_names[1],
177+
lineterm="",
169178
)
170179

180+
# Try to shorten common path prefix
171181
try:
172-
common_path_length = len(os.path.commonpath(view_names))
173-
if common_path_length <= 1:
174-
common_path_length = 0
175-
else:
176-
common_path_length += 1
177-
except ValueError:
178-
common_path_length = 0
179-
view_names = list(map(lambda name: name[common_path_length:], view_names))
180-
show_diff_output(diff, views[0], views[0].window(), f'{view_names[0]} -> {view_names[1]}', 'diff_views', 'diff_tabs_to_buffer')
181-
182-
def is_enabled(self, **kwargs):
182+
common_path = Path(*Path(view_names[0]).parts).parent
183+
common_prefix = str(common_path)
184+
if common_prefix and all(name.startswith(common_prefix) for name in view_names):
185+
view_names = [name[len(common_prefix) + 1 :] for name in view_names]
186+
except Exception:
187+
pass
188+
189+
show_diff_output(
190+
diff,
191+
views[0],
192+
views[0].window(),
193+
f"{view_names[0]} -> {view_names[1]}",
194+
"diff_views",
195+
"diff_tabs_to_buffer",
196+
)
197+
198+
def is_enabled(self, **kwargs) -> bool:
183199
return self.is_visible(**kwargs)
184200

185-
def is_visible(self, **kwargs):
186-
views = get_views_from_tab_context(self.view, **kwargs)
187-
return len(views) == 2
201+
def is_visible(self, **kwargs) -> bool:
202+
return len(get_views_from_tab_context(self.view, **kwargs)) == 2
188203

189-
def description(self, **kwargs):
204+
def description(self, **kwargs) -> str:
190205
selected_views = list(get_selected_views(self.view.window()))
191-
if len(selected_views) == 2:
192-
return 'Diff Selected Tabs...'
193-
194-
return 'Diff With Current Tab...'
206+
return "Diff Selected Tabs..." if len(selected_views) == 2 else "Diff With Current Tab..."

0 commit comments

Comments
 (0)