.format replaced by f-strings

This commit is contained in:
Vaclav Tvrdik
2026-09-26 20:25:50 +02:00
parent bb7ae537ea
commit af8c1dd6df
7 changed files with 130 additions and 86 deletions

View File

@@ -32,7 +32,7 @@ class Region:
return (self.a, self.b) == (other.a, other.b)
def __repr__(self):
return "Region({}, {})".format(self.a, self.b)
return f"Region({self.a}, {self.b})"
class Settings(dict):
@@ -240,7 +240,13 @@ def find_syntax_for_file(path, first_line=""):
def ui_info():
return {"system": {"style": "dark"}, "color_scheme": {"value": "Mariana.sublime-color-scheme", "resolved_value": "Packages/Color Scheme - Default/Mariana.sublime-color-scheme"}}
return {
"system": {"style": "dark"},
"color_scheme": {
"value": "Mariana.sublime-color-scheme",
"resolved_value": "Packages/Color Scheme - Default/Mariana.sublime-color-scheme",
},
}
def decode_value(text):

View File

@@ -11,7 +11,7 @@ def change(path, status="M"):
def flagged(*flags):
"""f0.py, f1.py ... with the given staged flags"""
return [git.FileChange("f{}.py".format(i), "M", flag, 1, 1, None) for i, flag in enumerate(flags)]
return [git.FileChange(f"f{i}.py", "M", flag, 1, 1, None) for i, flag in enumerate(flags)]
def option(monkeypatch, name, value):
@@ -28,7 +28,7 @@ def at_change(blocks, row):
view = sublime.View()
st = state.ViewState("/repo")
st.blocks = blocks
view.text = "\n" + "\n".join("l{}".format(i) for i in range(20))
view.text = "\n" + "\n".join(f"l{i}" for i in range(20))
view.sel().add(sublime.Region(commands.render.index_point(view, row)))
return view, st

View File

@@ -133,9 +133,9 @@ def test_help_popup_lists_every_key():
assert view.is_popup_visible()
for group in ("Navigate", "Panels", "Actions", "View"):
assert ">{}<".format(group) in html
assert f">{group}<" in html
for keys in ("] [", "n p", "o ⏎", "j k", "f", "c", "s", "u", "d", "C", "m", "r", "?", "esc"):
assert ">{}<".format(keys) in html
assert f">{keys}<" in html
assert "next / previous change" in html and "open the real file at this line" in html
# working-tree mode: nothing dimmed, every single-action row is a link
assert 'class="dim"' not in html and "uncommitted only" not in html
@@ -222,7 +222,7 @@ def test_minihtml_stylesheets_reject_unsupported_selectors():
for rule in css.split("}"):
head = rule.split("{")[0].strip()
for selector in filter(None, (s.strip() for s in head.split(","))):
assert not unsupported.search(selector), "{}: {}".format(name, selector)
assert not unsupported.search(selector), f"{name}: {selector}"
# --- header summary line ----------------------------------------------------
@@ -238,7 +238,26 @@ def test_summary_counts_staged_and_unstaged_files():
git.FileChange("c.py", "?", False, 1, 0, None),
]
render.render(view, st)
assert '<div class="summary">1 staged, 2 unstaged</div>' in header(view)
summary = header(view).split('<div class="summary">')[1].split("</div>")[0]
# the untracked file's whole content counts as insertions, the way git.changed_files records it
assert summary.startswith("1 staged, 2 unstaged · ")
assert '<span class="del">-2</span>' in summary and '<span class="ins">+3</span>' in summary
def test_summary_totals_skip_binary_files_but_still_count_them():
view = sublime.View()
render.init_view(view)
st = make_state()
st.files = [
git.FileChange("a.py", "M", False, 4, 1, None),
git.FileChange("logo.png", "M", False, None, None, None), # binary: no counts, still a file
]
render.render(view, st)
summary = header(view).split('<div class="summary">')[1].split("</div>")[0]
assert summary.startswith("0 staged, 2 unstaged · ")
assert '<span class="del">-1</span>' in summary and '<span class="ins">+4</span>' in summary
def test_summary_totals_a_commit_and_owns_the_date():

View File

@@ -120,7 +120,7 @@ def file_items(files):
if None in (change.insertions, change.deletions):
annotation = "binary"
else:
annotation = "-{} +{}".format(change.deletions, change.insertions)
annotation = f"-{change.deletions} +{change.insertions}"
items.append(sublime.QuickPanelItem(change.path, details, annotation, kind))
return items
@@ -132,14 +132,12 @@ def commit_items(commits):
annotation = c.short
if c.files is not None:
# merges and empty commits have no diffstat, so they show the hash alone
annotation = "{} changed -{} +{} {}".format(
file_count(c.files), c.deletions, c.insertions, c.short
)
annotation = f"{file_count(c.files)} changed -{c.deletions} +{c.insertions} {c.short}"
subject = c.subject or "(no subject)"
budget = max(COMMIT_ROW_WIDTH - len(annotation), MIN_SUBJECT_WIDTH)
if len(subject) > budget:
subject = subject[: budget - 2] + ".."
items.append(sublime.QuickPanelItem(subject, "{} · {}".format(c.author, c.date), annotation, COMMIT_KIND))
items.append(sublime.QuickPanelItem(subject, f"{c.author} · {c.date}", annotation, COMMIT_KIND))
return items
@@ -192,7 +190,7 @@ def refresh_listing(view, st, message=None, pick=None):
return
st.files = files
if not files:
render.render_message(view, st, "No uncommitted changes in {}".format(st.repo))
render.render_message(view, st, f"No uncommitted changes in {st.repo}")
return
index = state.find_change(files, path)
if index < 0:
@@ -227,10 +225,10 @@ class VdiffFilesCommand(sublime_plugin.WindowCommand):
return
kept = st.enter()
if not files:
render.render_message(view, st, "No uncommitted changes in {}".format(st.repo))
render.render_message(view, st, f"No uncommitted changes in {st.repo}")
status("no uncommitted changes")
return
placeholder = "Changed files in {}".format(os.path.basename(st.repo))
placeholder = f"Changed files in {os.path.basename(st.repo)}"
show_files_panel(window, view, st, files, kept, placeholder=placeholder)
run_async(lambda: git.changed_files(st.repo), done)
@@ -281,13 +279,13 @@ class VdiffCommitsCommand(sublime_plugin.WindowCommand):
info, files = result
kept = st.enter(info.sha, info)
if not files:
render.render_message(view, st, "Commit {} touches no files".format(info.short))
render.render_message(view, st, f"Commit {info.short} touches no files")
return
def back():
window.run_command("vdiff_commits", {"selected": index})
placeholder = "{} · {}".format(info.short, info.subject)
placeholder = f"{info.short} · {info.subject}"
on_cancel = back if index is not None else None
show_files_panel(window, view, st, files, kept, on_cancel=on_cancel, placeholder=placeholder)
@@ -321,14 +319,14 @@ def _git_action(verb, done_verb, operation, confirm=None, pick=None):
def act(view, window, st):
if st.in_commit or st.change is None:
status("{} works on uncommitted changes only".format(verb))
status(f"{verb} works on uncommitted changes only")
return
change = st.change
if confirm is not None and not confirm(change):
return
run_async(
lambda: operation(st.repo, change),
lambda _: refresh_listing(view, st, "{} {}".format(done_verb, change.path), pick=pick),
lambda _: refresh_listing(view, st, f"{done_verb} {change.path}", pick=pick),
)
return act
@@ -338,10 +336,11 @@ def _confirm_discard(change):
if not settings().get("confirm_discard", True):
return True
if change.status == "?":
message = "Delete the untracked file {}?\n\nThis cannot be undone.".format(change.path)
message = f"Delete the untracked file {change.path}?\n\nThis cannot be undone."
else:
message = "Discard all changes to {}?\n\nThe file goes back to its last committed version. This cannot be undone.".format(
change.path
message = (
f"Discard all changes to {change.path}?\n\n"
"The file goes back to its last committed version. This cannot be undone."
)
return sublime.ok_cancel_dialog(message, "Discard")
@@ -378,10 +377,10 @@ def _act_open(view, window, st):
return
path = os.path.join(st.repo, st.change.path)
if not os.path.exists(path):
status("{} does not exist in the working tree".format(st.change.path))
status(f"{st.change.path} does not exist in the working tree")
return
line = parse.line_for_open(st.lines, render.caret_index(view))
window.open_file("{}:{}".format(path, line), sublime.ENCODED_POSITION)
window.open_file(f"{path}:{line}", sublime.ENCODED_POSITION)
def _step_file(delta):
@@ -442,7 +441,7 @@ def _act_commit(view, window, st):
return
run_async(
lambda: git.commit(st.repo, message),
lambda _: refresh_listing(view, st, "committed {}".format(file_count(len(staged)))),
lambda _: refresh_listing(view, st, f"committed {file_count(len(staged))}"),
)
window.show_input_panel("Commit message", "", on_done, None, None)
@@ -468,7 +467,7 @@ def _act_amend(view, window, st):
return
done = "amended the last commit"
if staged:
done += " with {}".format(file_count(len(staged)))
done += f" with {file_count(len(staged))}"
run_async(
# nothing staged makes this a pure reword, which git refuses on an empty commit
lambda: git.amend(st.repo, text + newline + body, allow_empty=not staged),
@@ -520,7 +519,7 @@ class VdiffActionCommand(sublime_plugin.TextCommand):
def run(self, edit, action):
act = ACTIONS.get(action)
if act is None:
status("unknown action {}".format(action))
status(f"unknown action {action}")
return
st = state.get(self.view) or restore_view(self.view)
act(self.view, self.view.window(), st)
@@ -558,7 +557,7 @@ def restore_view(view):
if index >= 0:
show_file(view, st, files[index])
else:
render.render_message(view, st, "{} has no uncommitted changes any more".format(path))
render.render_message(view, st, f"{path} has no uncommitted changes any more")
run_async(lambda: git.changed_files(repo), done)
return st

View File

@@ -44,15 +44,13 @@ class GitError(Exception):
self.stderr = stderr
self.code = code
first = stderr.strip().splitlines()
super().__init__(first[0] if first else "git exited with {}".format(code))
super().__init__(first[0] if first else f"git exited with {code}")
def run(repo, args, ok_codes=(0,)):
"""Run git in `repo`, return stdout as text"""
cmd = [GIT_BINARY, "-c", "core.quotepath=off", "--no-pager"] + list(args)
proc = subprocess.run(
cmd, cwd=repo, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8", errors="replace"
)
proc = subprocess.run(cmd, cwd=repo, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8", errors="replace")
if proc.returncode not in ok_codes:
raise GitError(proc.stderr, proc.returncode)
return proc.stdout
@@ -201,7 +199,7 @@ def recent_commits(repo, count):
def commit_with_files(repo, sha):
"""(CommitInfo, [FileChange]) of one commit vs its first parent, files sorted by path; two git calls"""
show = ["show", "--first-parent", "-m", "-z"]
text = run(repo, show + ["--format={}%x00".format(_INFO_FORMAT), _DATE_FORMAT, "--name-status", sha])
text = run(repo, show + [f"--format={_INFO_FORMAT}%x00", _DATE_FORMAT, "--name-status", sha])
# git terminates the header with its own NUL and a newline before the file list
info_text, _, files_text = text.partition("\0")
files_text = files_text.lstrip("\0\n")
@@ -222,7 +220,7 @@ def commit_with_files(repo, sha):
def _in_tree(repo, ref, path):
"""Whether `path` exists in the tree of `ref`"""
try:
run(repo, ["cat-file", "-e", "{}:{}".format(ref, path)])
run(repo, ["cat-file", "-e", f"{ref}:{path}"])
return True
except GitError:
return False

View File

@@ -60,33 +60,49 @@ FALLBACK_RULES = [
Key = namedtuple("Key", "keys label short actions")
KEY_GROUPS = [
("Navigate", True, [
(
"Navigate",
True,
[
Key("] [", "next / previous change (wraps)", "change", ("next_change", "prev_change")),
Key("n p", "next / previous file (wraps)", "file", ("next_file", "prev_file")),
Key("o \u23ce", "open the real file at this line", "open", ("open",)),
Key("j k", "down / up one line (vim)", "", ("line_down", "line_up")),
]),
("Panels", True, [
],
),
(
"Panels",
True,
[
Key("f", "changed files", "files", ("files",)),
Key("c", "recent commits", "commits", ("commits",)),
]),
("Actions", False, [
],
),
(
"Actions",
False,
[
Key("s", "stage", "stage", ("stage",)),
Key("u", "unstage", "unstage", ("unstage",)),
Key("d", "discard", "discard", ("discard",)),
Key("C", "commit the staged files", "commit", ("commit",)),
Key("m", "amend the last commit", "amend", ("amend",)),
]),
("View", True, [
],
),
(
"View",
True,
[
Key("r", "refresh", "refresh", ("refresh",)),
Key("?", "this list", "keys", ("help",)),
Key("esc", "close the view", "", ("close",)),
]),
],
),
]
KEYS = [key for _, _, group in KEY_GROUPS for key in group]
KEY_HINTS = " ".join("{} {}".format(k.keys, k.short) for k in KEYS if k.short)
KEY_HINTS = " ".join(f"{k.keys} {k.short}" for k in KEYS if k.short)
# the popup font is the editor's, so the key column is padded to a fixed width in characters
_KEY_WIDTH = max(len(k.keys) for k in KEYS)
@@ -242,7 +258,7 @@ def render(view, st):
view.add_regions(KEY_INS_CHAR, ins_chars, SCOPE_INS_CHAR, flags=sublime.DRAW_NO_OUTLINE)
ins, dele = parse.counts(lines)
_finish(view, st, ins, dele, "⟐ {}".format(os.path.basename(st.change.path)))
_finish(view, st, ins, dele, f"⟐ {os.path.basename(st.change.path)}")
view.sel().clear()
view.sel().add(sublime.Region(index_point(view, 0)))
view.set_viewport_position((0, 0), False)
@@ -311,6 +327,11 @@ def _char_regions(lines, blocks, starts):
return deleted, inserted
def _totals(files):
"""Summed numstat counts; binary files carry None counts and add nothing"""
return sum(c.insertions or 0 for c in files), sum(c.deletions or 0 for c in files)
def _summary_html(st):
"""Line above the header: the whole commit on one line, or how much of the tree is staged"""
if not st.files and not st.commit:
@@ -320,8 +341,12 @@ def _summary_html(st):
else:
# staged means "has something staged": a partially staged file counts as staged only
staged = sum(1 for c in st.files if c.staged)
text = "{} staged, {} unstaged".format(staged, len(st.files) - staged)
return '<div class="summary">{}</div>'.format(text)
insertions, deletions = _totals(st.files)
text = (
f"{staged} staged, {len(st.files) - staged} unstaged"
f' · <span class="del">-{deletions}</span> <span class="ins">+{insertions}</span>'
)
return f'<div class="summary">{text}</div>'
def _commit_parts(st):
@@ -329,22 +354,17 @@ def _commit_parts(st):
info = st.commit
parts = []
if info:
parts.append('<span class="hash">{}</span>'.format(html.escape(info.short)))
parts.append(f'<span class="hash">{html.escape(info.short)}</span>')
parts.append(html.escape(info.author))
parts.append('<span class="when">{}</span>'.format(html.escape(info.date)))
# binary files carry None counts but still changed, so they count toward the file total
insertions = sum(c.insertions or 0 for c in st.files)
deletions = sum(c.deletions or 0 for c in st.files)
parts.append(
'{} changed <span class="del">-{}</span> <span class="ins">+{}</span>'.format(
file_count(len(st.files)), deletions, insertions
)
)
parts.append(f'<span class="when">{html.escape(info.date)}</span>')
# binary files add nothing to the line totals but still count toward the file total
insertions, deletions = _totals(st.files)
parts.append(f"{file_count(len(st.files))} changed" f' <span class="del">-{deletions}</span> <span class="ins">+{insertions}</span>')
if info:
message = html.escape(info.subject)
body = info.body.strip().splitlines()
if body:
message += ' <span class="body">— {}</span>'.format(html.escape(body[0]))
message += f' <span class="body">— {html.escape(body[0])}</span>'
parts.append(message)
return parts
@@ -355,14 +375,14 @@ def header_html(st, ins, dele):
if change is None:
title = '<span class="path">vDiff</span>'
else:
title = '<span class="path">{}</span>'.format(html.escape(change.path))
title = f'<span class="path">{html.escape(change.path)}</span>'
if change.old_path:
title += ' <span class="old">← {}</span>'.format(html.escape(change.old_path))
title += '&nbsp;&nbsp;<span class="del">-{}</span> <span class="ins">+{}</span>'.format(dele, ins)
title += f' <span class="old">← {html.escape(change.old_path)}</span>'
title += f'&nbsp;&nbsp;<span class="del">-{dele}</span> <span class="ins">+{ins}</span>'
if not st.in_commit:
state = "staged" if change.staged else STATUS_NAMES.get(change.status, "")
if state:
title += '&nbsp;&nbsp;<span class="state">{}</span>'.format(state)
title += f'&nbsp;&nbsp;<span class="state">{state}</span>'
actions = []
if change is not None and not st.in_commit:
@@ -375,8 +395,9 @@ def header_html(st, ins, dele):
actions.append('<a href="commits">Commits</a>')
actions.append('<a href="help" title="keys">?</a>')
return '<body id="vdiff-header"><style>{}</style>{}<div>{}<span class="actions">{}</span></div></body>'.format(
HEADER_CSS, _summary_html(st), title, "".join(actions)
return (
f'<body id="vdiff-header"><style>{HEADER_CSS}</style>{_summary_html(st)}'
f'<div>{title}<span class="actions">{"".join(actions)}</span></div></body>'
)
@@ -406,17 +427,18 @@ def help_html(st):
heading = html.escape(title)
if dim:
heading += ' <span class="note">uncommitted only</span>'
parts.append('<div class="group">{}</div>'.format(heading))
parts.append(f'<div class="group">{heading}</div>')
# one class per element: a compound selector would silently drop the rest of HELP_CSS
row_class, key_class = ("dim", "kdim") if dim else ("row", "k")
for key in group:
row = '<span class="{}">{}</span>{}{}'.format(
key_class, html.escape(key.keys), "&nbsp;" * (_KEY_WIDTH - len(key.keys) + 2), html.escape(key.label)
row = (
f'<span class="{key_class}">{html.escape(key.keys)}</span>'
f'{"&nbsp;" * (_KEY_WIDTH - len(key.keys) + 2)}{html.escape(key.label)}'
)
if len(key.actions) == 1 and not dim:
row = '<a href="{}">{}</a>'.format(key.actions[0], row)
parts.append('<div class="{}">{}</div>'.format(row_class, row))
return '<body id="vdiff-help"><style>{}</style>{}</body>'.format(HELP_CSS, "".join(parts))
row = f'<a href="{key.actions[0]}">{row}</a>'
parts.append(f'<div class="{row_class}">{row}</div>')
return f'<body id="vdiff-help"><style>{HELP_CSS}</style>{"".join(parts)}</body>'
def show_help(view, st):

View File

@@ -29,12 +29,12 @@ def apply_git_binary():
def file_count(count):
"""'1 file' / '3 files' for a message"""
return "{} {}".format(count, "file" if count == 1 else "files")
return f'{count} {"file" if count == 1 else "files"}'
def status(message):
"""Status-bar message with the plugin prefix"""
sublime.status_message("vDiff: {}".format(message))
sublime.status_message(f"vDiff: {message}")
def run_async(work, done):