Skip to content

Commit 6ebcffd

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

File tree

2 files changed

+104
-91
lines changed

2 files changed

+104
-91
lines changed

Diff/.python-version

Lines changed: 0 additions & 1 deletion
This file was deleted.

Diff/diff.py

Lines changed: 104 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,45 @@
1+
from __future__ import annotations
2+
13
import difflib
2-
import os
34
import time
5+
from pathlib import Path
6+
from typing import Iterable, Iterator, List, Optional
47

58
import sublime
69
import sublime_plugin
710

811

9-
def splitlines_keep_ends(text):
12+
def splitlines_keep_ends(text: str) -> List[str]:
13+
"""
14+
Split text into lines but preserve newline endings.
15+
Required for difflib to work correctly.
16+
"""
1017
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-
18+
for i in range(len(lines) - 1):
19+
lines[i] += '\n'
1820
return lines
1921

2022

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
23+
def read_file_lines(fname: str | Path) -> List[str]:
24+
"""Read a UTF-8 file and return its lines with newline endings preserved."""
25+
path = Path(fname)
26+
text = path.read_text(encoding="utf-8")
27+
lines = splitlines_keep_ends(text)
2828
add_no_eol_warning_if_applicable(lines)
29-
3029
return lines
3130

3231

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
32+
def add_no_eol_warning_if_applicable(lines: List[str]) -> None:
33+
"""
34+
Append a note if file doesn't end with newline.
35+
difflib misbehaves otherwise.
36+
"""
37+
if lines and lines[-1] and not lines[-1].endswith('\n'):
3738
lines[-1] += '\n\\ No newline at end of file\n'
3839

3940

4041
class DiffFilesCommand(sublime_plugin.WindowCommand):
41-
42-
def run(self, files):
42+
def run(self, files: List[str]) -> None:
4343
if len(files) != 2:
4444
return
4545

@@ -50,23 +50,30 @@ def run(self, files):
5050
sublime.status_message("Diff only works with UTF-8 files")
5151
return
5252

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

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')
57+
diff = difflib.unified_diff(
58+
a, b, files[1], files[0], adate, bdate, lineterm=""
59+
)
60+
show_diff_output(
61+
diff,
62+
None,
63+
self.window,
64+
f"{a_path.name} -> {b_path.name}",
65+
"diff_files",
66+
"diff_files_to_buffer",
67+
)
5868

59-
def is_visible(self, files):
69+
def is_visible(self, files: List[str]) -> bool:
6070
return len(files) == 2
6171

6272

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

@@ -77,24 +84,31 @@ def run(self, edit):
7784
return
7885

7986
b = get_lines_for_view(self.view)
80-
8187
add_no_eol_warning_if_applicable(b)
8288

83-
adate = time.ctime(os.stat(fname).st_mtime)
89+
adate = time.ctime(Path(fname).stat().st_mtime)
8490
bdate = time.ctime()
8591

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')
92+
diff = difflib.unified_diff(a, b, fname, fname, adate, bdate, lineterm="")
93+
name = f"Unsaved Changes: {Path(fname).name}"
94+
show_diff_output(diff, self.view, self.view.window(), name, "unsaved_changes", "diff_changes_to_buffer")
8995

90-
def is_enabled(self):
96+
def is_enabled(self) -> bool:
9197
return self.view.is_dirty() and self.view.file_name() is not None
9298

9399

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

97-
if difftxt == "":
111+
if not difftxt:
98112
sublime.status_message("No changes")
99113
return
100114

@@ -107,88 +121,88 @@ def show_diff_output(diff, view, win, name, panel_name, buffer_setting_name):
107121
else:
108122
v = win.create_output_panel(panel_name)
109123
if view:
110-
v.settings().set('word_wrap', view.settings().get('word_wrap'))
124+
v.settings().set("word_wrap", view.settings().get("word_wrap"))
111125

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

115129
if not use_buffer:
116-
win.run_command('show_panel', {'panel': f'output.{panel_name}'})
130+
win.run_command("show_panel", {"panel": f"output.{panel_name}"})
117131

118132

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
133+
def get_view_from_tab_context(active_view: sublime.View, **kwargs) -> sublime.View:
134+
"""Return the view associated with the clicked tab."""
135+
if "group" in kwargs and "index" in kwargs:
136+
return active_view.window().views_in_group(kwargs["group"])[kwargs["index"]]
137+
return active_view
124138

125139

126-
def get_views_from_tab_context(active_view, **kwargs):
140+
def get_views_from_tab_context(active_view: sublime.View, **kwargs) -> List[sublime.View]:
141+
"""Return selected views, preserving right-click order."""
127142
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]
143+
if "group" in kwargs and "index" in kwargs:
144+
tab_view = get_view_from_tab_context(active_view, **kwargs)
145+
return [v for v in selected_views if v.id() != tab_view.id()] + [tab_view]
133146
return selected_views
134147

135148

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

139153

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

143157

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

147162

148163
class DiffViewsCommand(sublime_plugin.TextCommand):
149-
def run(self, edit, **kwargs):
164+
def run(self, edit: sublime.Edit, **kwargs) -> None:
150165
views = get_views_from_tab_context(self.view, **kwargs)
151166
if len(views) != 2:
152167
return
153168

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])
169+
view_names = [get_name_for_view(v) for v in views]
170+
from_lines, to_lines = map(get_lines_for_view, views)
161171
add_no_eol_warning_if_applicable(from_lines)
162172
add_no_eol_warning_if_applicable(to_lines)
163173

164174
diff = difflib.unified_diff(
165175
from_lines,
166176
to_lines,
167177
fromfile=view_names[0],
168-
tofile=view_names[1]
178+
tofile=view_names[1],
179+
lineterm="",
169180
)
170181

182+
# Try to shorten common path prefix
171183
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):
184+
common_path = Path(*Path(view_names[0]).parts).parent
185+
common_prefix = str(common_path)
186+
if common_prefix and all(name.startswith(common_prefix) for name in view_names):
187+
view_names = [name[len(common_prefix) + 1 :] for name in view_names]
188+
except Exception:
189+
pass
190+
191+
show_diff_output(
192+
diff,
193+
views[0],
194+
views[0].window(),
195+
f"{view_names[0]} -> {view_names[1]}",
196+
"diff_views",
197+
"diff_tabs_to_buffer",
198+
)
199+
200+
def is_enabled(self, **kwargs) -> bool:
183201
return self.is_visible(**kwargs)
184202

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

189-
def description(self, **kwargs):
206+
def description(self, **kwargs) -> str:
190207
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...'
208+
return "Diff Selected Tabs..." if len(selected_views) == 2 else "Diff With Current Tab..."

0 commit comments

Comments
 (0)