Merge pull request #140 from MightyCreak/static-types

Convert some functions to static typing
This commit is contained in:
Creak 2022-01-08 18:20:33 -05:00 committed by GitHub
commit 8ac41b0b90
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 173 additions and 159 deletions

View File

@ -1,2 +1,3 @@
[mypy] [mypy]
warn_unused_ignores = True warn_unused_ignores = True
disallow_incomplete_defs = True

View File

@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Renamed application name from "Diffuse Merge Tool" to "Diffuse" - Renamed application name from "Diffuse Merge Tool" to "Diffuse"
- Linters can be run sooner (before installation) - Linters can be run sooner (before installation)
- Better messages when an error occurs while parsing the config file - Better messages when an error occurs while parsing the config file
- Start converting the code to static types
### Fixed ### Fixed
- Removed the lasting lint errors (i.e. in main.py) - Removed the lasting lint errors (i.e. in main.py)

View File

@ -18,11 +18,12 @@
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
from gettext import gettext as _ from gettext import gettext as _
from typing import Final
APP_NAME = 'Diffuse' APP_NAME: Final[str] = 'Diffuse'
COPYRIGHT = '''{copyright} © 2006-2019 Derrick Moser COPYRIGHT: Final[str] = '''{copyright} © 2006-2019 Derrick Moser
{copyright} © 2015-2021 Romain Failliot'''.format(copyright=_("Copyright")) {copyright} © 2015-2021 Romain Failliot'''.format(copyright=_("Copyright"))
WEBSITE = 'https://mightycreak.github.io/diffuse/' WEBSITE: Final[str] = 'https://mightycreak.github.io/diffuse/'
# Constants are set in main() # Constants are set in main()
VERSION = '0.0.0' VERSION: str = '0.0.0'

View File

@ -32,7 +32,7 @@ from gi.repository import GObject, Gtk # type: ignore # noqa: E402
# the about dialog # the about dialog
class AboutDialog(Gtk.AboutDialog): class AboutDialog(Gtk.AboutDialog):
def __init__(self): def __init__(self) -> None:
Gtk.AboutDialog.__init__(self) Gtk.AboutDialog.__init__(self)
self.set_logo_icon_name('io.github.mightycreak.Diffuse') self.set_logo_icon_name('io.github.mightycreak.Diffuse')
self.set_program_name(constants.APP_NAME) self.set_program_name(constants.APP_NAME)
@ -104,13 +104,13 @@ class FileChooserDialog(Gtk.FileChooserDialog):
def set_encoding(self, encoding): def set_encoding(self, encoding):
self.encoding.set_text(encoding) self.encoding.set_text(encoding)
def get_encoding(self): def get_encoding(self) -> str:
return self.encoding.get_text() return self.encoding.get_text()
def get_revision(self): def get_revision(self) -> str:
return self.revision.get_text() return self.revision.get_text()
def get_filename(self): def get_filename(self) -> str:
# convert from UTF-8 string to unicode # convert from UTF-8 string to unicode
return Gtk.FileChooserDialog.get_filename(self) return Gtk.FileChooserDialog.get_filename(self)

View File

@ -26,6 +26,7 @@ import stat
import webbrowser import webbrowser
from gettext import gettext as _ from gettext import gettext as _
from typing import Optional
from urllib.parse import urlparse from urllib.parse import urlparse
from diffuse import constants from diffuse import constants
@ -33,6 +34,7 @@ from diffuse import utils
from diffuse.dialogs import AboutDialog, FileChooserDialog, NumericDialog, SearchDialog from diffuse.dialogs import AboutDialog, FileChooserDialog, NumericDialog, SearchDialog
from diffuse.preferences import Preferences from diffuse.preferences import Preferences
from diffuse.resources import theResources from diffuse.resources import theResources
from diffuse.utils import LineEnding
from diffuse.vcs.vcs_registry import VcsRegistry from diffuse.vcs.vcs_registry import VcsRegistry
from diffuse.widgets import FileDiffViewerBase from diffuse.widgets import FileDiffViewerBase
from diffuse.widgets import createMenu, LINE_MODE, CHAR_MODE, ALIGN_MODE from diffuse.widgets import createMenu, LINE_MODE, CHAR_MODE, ALIGN_MODE
@ -118,7 +120,7 @@ class Diffuse(Gtk.Window):
class FileDiffViewer(FileDiffViewerBase): class FileDiffViewer(FileDiffViewerBase):
# pane header # pane header
class PaneHeader(Gtk.Box): class PaneHeader(Gtk.Box):
def __init__(self): def __init__(self) -> None:
Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL, spacing=0) Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL, spacing=0)
_append_buttons(self, Gtk.IconSize.MENU, [ _append_buttons(self, Gtk.IconSize.MENU, [
[Gtk.STOCK_OPEN, self.button_cb, 'open', _('Open File...')], [Gtk.STOCK_OPEN, self.button_cb, 'open', _('Open File...')],
@ -171,7 +173,7 @@ class Diffuse(Gtk.Window):
# pane footer # pane footer
class PaneFooter(Gtk.Box): class PaneFooter(Gtk.Box):
def __init__(self): def __init__(self) -> None:
Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL, spacing=0) Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL, spacing=0)
self.cursor = label = Gtk.Label.new() self.cursor = label = Gtk.Label.new()
self.cursor.set_size_request(-1, -1) self.cursor.set_size_request(-1, -1)
@ -212,11 +214,11 @@ class Diffuse(Gtk.Window):
# set the format label # set the format label
def setFormat(self, s): def setFormat(self, s):
v = [] v = []
if s & utils.DOS_FORMAT: if s & LineEnding.DOS_FORMAT:
v.append('DOS') v.append('DOS')
if s & utils.MAC_FORMAT: if s & LineEnding.MAC_FORMAT:
v.append('Mac') v.append('Mac')
if s & utils.UNIX_FORMAT: if s & LineEnding.UNIX_FORMAT:
v.append('Unix') v.append('Unix')
self.format.set_text('/'.join(v)) self.format.set_text('/'.join(v))
@ -1108,7 +1110,7 @@ class Diffuse(Gtk.Window):
self.quit_cb(widget, data) self.quit_cb(widget, data)
# convenience method to request confirmation when closing the last tab # convenience method to request confirmation when closing the last tab
def _confirm_tab_close(self): def _confirm_tab_close(self) -> bool:
dialog = utils.MessageDialog( dialog = utils.MessageDialog(
self.get_toplevel(), self.get_toplevel(),
Gtk.MessageType.WARNING, Gtk.MessageType.WARNING,
@ -1193,7 +1195,7 @@ class Diffuse(Gtk.Window):
self.setSyntax(s) self.setSyntax(s)
# create an empty viewer with 'n' panes # create an empty viewer with 'n' panes
def newFileDiffViewer(self, n): def newFileDiffViewer(self, n: int) -> FileDiffViewer:
self.viewer_count += 1 self.viewer_count += 1
tabname = _('File Merge %d') % (self.viewer_count, ) tabname = _('File Merge %d') % (self.viewer_count, )
tab = NotebookTab(tabname, Gtk.STOCK_FILE) tab = NotebookTab(tabname, Gtk.STOCK_FILE)
@ -1338,13 +1340,13 @@ class Diffuse(Gtk.Window):
) )
# close all tabs without differences # close all tabs without differences
def closeOnSame(self): def closeOnSame(self) -> None:
for i in range(self.notebook.get_n_pages() - 1, -1, -1): for i in range(self.notebook.get_n_pages() - 1, -1, -1):
if not self.notebook.get_nth_page(i).hasDifferences(): if not self.notebook.get_nth_page(i).hasDifferences():
self.notebook.remove_page(i) self.notebook.remove_page(i)
# returns True if the application can safely quit # returns True if the application can safely quit
def confirmQuit(self): def confirmQuit(self) -> bool:
nb = self.notebook nb = self.notebook
return self.confirmCloseViewers([nb.get_nth_page(i) for i in range(nb.get_n_pages())]) return self.confirmCloseViewers([nb.get_nth_page(i) for i in range(nb.get_n_pages())])
@ -1356,7 +1358,7 @@ class Diffuse(Gtk.Window):
return True return True
# returns the currently focused viewer # returns the currently focused viewer
def getCurrentViewer(self): def getCurrentViewer(self) -> Optional[Gtk.Widget]:
return self.notebook.get_nth_page(self.notebook.get_current_page()) return self.notebook.get_nth_page(self.notebook.get_current_page())
# callback for the open file menu item # callback for the open file menu item
@ -1570,7 +1572,7 @@ class Diffuse(Gtk.Window):
self.getCurrentViewer().go_to_line_cb(widget, data) self.getCurrentViewer().go_to_line_cb(widget, data)
# notify all viewers of changes to the preferences # notify all viewers of changes to the preferences
def preferences_updated(self): def preferences_updated(self) -> None:
n = self.notebook.get_n_pages() n = self.notebook.get_n_pages()
self.notebook.set_show_tabs(self.prefs.getBool('tabs_always_show') or n > 1) self.notebook.set_show_tabs(self.prefs.getBool('tabs_always_show') or n > 1)
for i in range(n): for i in range(n):
@ -1712,7 +1714,7 @@ def _append_buttons(box, size, specs):
# constructs a full URL for the named file # constructs a full URL for the named file
def _path2url(path, proto='file'): def _path2url(path: str, proto: str = 'file') -> str:
r = [proto, ':///'] r = [proto, ':///']
s = os.path.abspath(path) s = os.path.abspath(path)
i = 0 i = 0

View File

@ -199,7 +199,7 @@ class Preferences:
self.path = path self.path = path
if os.path.isfile(self.path): if os.path.isfile(self.path):
try: try:
with open(self.path, 'r', encoding='utf-8') as f: with open(self.path, 'r', encoding='utf-8') as f:
ss = utils.readconfiglines(f) ss = utils.readconfiglines(f)
for j, s in enumerate(ss): for j, s in enumerate(ss):
try: try:
@ -342,7 +342,7 @@ class Preferences:
if tpl[0] in ['Font', 'Integer']: if tpl[0] in ['Font', 'Integer']:
entry = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 0) entry = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 0)
if tpl[0] == 'Font': if tpl[0] == 'Font':
button = _FontButton() button = Gtk.FontButton()
button.set_font(self.string_prefs[tpl[1]]) button.set_font(self.string_prefs[tpl[1]])
else: else:
button = Gtk.SpinButton.new( button = Gtk.SpinButton.new(
@ -368,28 +368,28 @@ class Preferences:
return table return table
# get/set methods to manipulate the preference values # get/set methods to manipulate the preference values
def getBool(self, name): def getBool(self, name: str) -> bool:
return self.bool_prefs[name] return self.bool_prefs[name]
def setBool(self, name, value): def setBool(self, name: str, value: bool) -> None:
self.bool_prefs[name] = value self.bool_prefs[name] = value
def getInt(self, name): def getInt(self, name: str) -> int:
return self.int_prefs[name] return self.int_prefs[name]
def getString(self, name): def getString(self, name: str) -> str:
return self.string_prefs[name] return self.string_prefs[name]
def setString(self, name, value): def setString(self, name: str, value: str) -> None:
self.string_prefs[name] = value self.string_prefs[name] = value
def getEncodings(self): def getEncodings(self):
return self.encodings return self.encodings
def _getDefaultEncodings(self): def _getDefaultEncodings(self) -> list[str]:
return self.string_prefs['encoding_auto_detect_codecs'].split() return self.string_prefs['encoding_auto_detect_codecs'].split()
def getDefaultEncoding(self): def getDefaultEncoding(self) -> str:
return self.string_prefs['encoding_default_codec'] return self.string_prefs['encoding_default_codec']
# attempt to convert a string to unicode from an unknown encoding # attempt to convert a string to unicode from an unknown encoding
@ -412,7 +412,7 @@ class Preferences:
# cygwin and native applications can be used on windows, use this method # cygwin and native applications can be used on windows, use this method
# to convert a path to the usual form expected on sys.platform # to convert a path to the usual form expected on sys.platform
def convertToNativePath(self, s): def convertToNativePath(self, s: str) -> str:
if utils.isWindows() and s.find('/') >= 0: if utils.isWindows() and s.find('/') >= 0:
# treat as a cygwin path # treat as a cygwin path
s = s.replace(os.sep, '/') s = s.replace(os.sep, '/')
@ -436,15 +436,6 @@ class Preferences:
return s return s
# adaptor class to allow a Gtk.FontButton to be read like a Gtk.Entry
class _FontButton(Gtk.FontButton):
def __init__(self):
Gtk.FontButton.__init__(self)
def get_text(self):
return self.get_font_name()
# text entry widget with a button to help pick file names # text entry widget with a button to help pick file names
class _FileEntry(Gtk.Box): class _FileEntry(Gtk.Box):
def __init__(self, parent, title): def __init__(self, parent, title):
@ -475,8 +466,8 @@ class _FileEntry(Gtk.Box):
self.entry.set_text(dialog.get_filename()) self.entry.set_text(dialog.get_filename())
dialog.destroy() dialog.destroy()
def set_text(self, s): def set_text(self, s: str) -> None:
self.entry.set_text(s) self.entry.set_text(s)
def get_text(self): def get_text(self) -> str:
return self.entry.get_text() return self.entry.get_text()

View File

@ -31,6 +31,7 @@ import shlex
from distutils import util from distutils import util
from gettext import gettext as _ from gettext import gettext as _
from typing import Final
from diffuse import utils from diffuse import utils
@ -529,7 +530,7 @@ class Resources:
# colour resources # colour resources
class _Colour: class _Colour:
def __init__(self, r, g, b, a=1.0): def __init__(self, r: float, g: float, b: float, a: float = 1.0):
# the individual colour components as floats in the range [0, 1] # the individual colour components as floats in the range [0, 1]
self.red = r self.red = r
self.green = g self.green = g
@ -601,4 +602,4 @@ class _SyntaxParser:
return state_name, blocks return state_name, blocks
theResources = Resources() theResources: Final[Resources] = Resources()

View File

@ -23,7 +23,9 @@ import locale
import subprocess import subprocess
import traceback import traceback
from enum import IntFlag
from gettext import gettext as _ from gettext import gettext as _
from typing import Final, Optional, TextIO
from diffuse import constants from diffuse import constants
from diffuse.resources import theResources from diffuse.resources import theResources
@ -74,38 +76,38 @@ class EncodingMenu(Gtk.Box):
return self.encodings[i] if i >= 0 else None return self.encodings[i] if i >= 0 else None
# platform test def isWindows() -> bool:
def isWindows(): '''Returns true if OS is Windows; otherwise false.'''
return os.name == 'nt' return os.name == 'nt'
def _logPrintOutput(msg): def _logPrintOutput(msg: str) -> None:
if theResources.getOptionAsBool('log_print_output'): if theResources.getOptionAsBool('log_print_output'):
print(msg, file=sys.stderr) print(msg, file=sys.stderr)
if theResources.getOptionAsBool('log_print_stack'): if theResources.getOptionAsBool('log_print_stack'):
traceback.print_stack() traceback.print_stack()
# convenience function to display debug messages def logDebug(msg: str) -> None:
def logDebug(msg): '''Report debug message.'''
_logPrintOutput(f'DEBUG: {msg}') _logPrintOutput(f'DEBUG: {msg}')
# report error messages def logError(msg: str) -> None:
def logError(msg): '''Report error message.'''
_logPrintOutput(f'ERROR: {msg}') _logPrintOutput(f'ERROR: {msg}')
# report error messages and show dialog def logErrorAndDialog(msg: str, parent: Gtk.Widget = None) -> None:
def logErrorAndDialog(msg, parent=None): '''Report error message and show dialog.'''
logError(msg) logError(msg)
dialog = MessageDialog(parent, Gtk.MessageType.ERROR, msg) dialog = MessageDialog(parent, Gtk.MessageType.ERROR, msg)
dialog.run() dialog.run()
dialog.destroy() dialog.destroy()
# create nested subdirectories and return the complete path def make_subdirs(p: str, ss: list[str]) -> str:
def make_subdirs(p, ss): '''Create nested subdirectories and return the complete path.'''
for s in ss: for s in ss:
p = os.path.join(p, s) p = os.path.join(p, s)
if not os.path.exists(p): if not os.path.exists(p):
@ -116,16 +118,16 @@ def make_subdirs(p, ss):
return p return p
# returns the Windows drive or share from a from an absolute path def _drive_from_path(path: str) -> str:
def _drive_from_path(path): '''Returns the Windows drive or share from a from an absolute path.'''
d = path.split(os.sep) d = path.split(os.sep)
if len(d) > 3 and d[0] == '' and d[1] == '': if len(d) > 3 and d[0] == '' and d[1] == '':
return os.path.join(d[:4]) return os.path.join(*d[:4])
return d[0] return d[0]
# constructs a relative path from 'a' to 'b', both should be absolute paths def relpath(a: str, b: str) -> str:
def relpath(a, b): '''Constructs a relative path from 'a' to 'b', both should be absolute paths.'''
if isWindows(): if isWindows():
if _drive_from_path(a) != _drive_from_path(b): if _drive_from_path(a) != _drive_from_path(b):
return b return b
@ -151,12 +153,12 @@ def safeRelativePath(abspath1, name, prefs, cygwin_pref):
return s return s
# escape arguments for use with bash def _bash_escape(s: str) -> str:
def _bash_escape(s): '''Escape arguments for use with bash.'''
return "'" + s.replace("'", "'\\''") + "'" return "'" + s.replace("'", "'\\''") + "'"
def _use_flatpak(): def _use_flatpak() -> bool:
return theResources.getOptionAsBool('use_flatpak') return theResources.getOptionAsBool('use_flatpak')
@ -201,9 +203,8 @@ def popenRead(dn, cmd, prefs, bash_pref, success_results=None):
return s return s
# returns the number of characters in the string excluding any line ending def len_minus_line_ending(s: str) -> int:
# characters '''Returns the number of characters in the string excluding any line ending characters.'''
def len_minus_line_ending(s):
if s is None: if s is None:
return 0 return 0
n = len(s) n = len(s)
@ -214,15 +215,15 @@ def len_minus_line_ending(s):
return n return n
# returns the string without the line ending characters def strip_eol(s: str) -> str:
def strip_eol(s): '''Returns the string without the line ending characters.'''
if s: if s:
s = s[:len_minus_line_ending(s)] s = s[:len_minus_line_ending(s)]
return s return s
# returns the list of strings without line ending characters def _strip_eols(ss: list[str]) -> list[str]:
def _strip_eols(ss): '''Returns the list of strings without line ending characters.'''
return [strip_eol(s) for s in ss] return [strip_eol(s) for s in ss]
@ -232,12 +233,12 @@ def popenReadLines(dn, cmd, prefs, bash_pref, success_results=None):
dn, cmd, prefs, bash_pref, success_results).decode('utf-8', errors='ignore'))) dn, cmd, prefs, bash_pref, success_results).decode('utf-8', errors='ignore')))
def readconfiglines(fd): def readconfiglines(fd: TextIO) -> list[str]:
return fd.read().replace('\r', '').split('\n') return fd.read().replace('\r', '').split('\n')
# escape special glob characters def globEscape(s: str) -> str:
def globEscape(s): '''Escape special glob characters.'''
m = {c: f'[{c}]' for c in '[]?*'} m = {c: f'[{c}]' for c in '[]?*'}
return ''.join([m.get(c, c) for c in s]) return ''.join([m.get(c, c) for c in s])
@ -272,25 +273,25 @@ def splitlines(text: str) -> list[str]:
# also recognize old Mac OS line endings # also recognize old Mac OS line endings
def readlines(fd): def readlines(fd: TextIO) -> list[str]:
return _strip_eols(splitlines(fd.read())) return _strip_eols(splitlines(fd.read()))
# map an encoding name to its standard form def norm_encoding(e: Optional[str]) -> Optional[str]:
def norm_encoding(e): '''Map an encoding name to its standard form.'''
if e is not None: if e is not None:
return e.replace('-', '_').lower() return e.replace('-', '_').lower()
return None return None
def null_to_empty(s): def null_to_empty(s: Optional[str]) -> str:
if s is None: if s is None:
s = '' s = ''
return s return s
# utility method to step advance an adjustment # utility method to step advance an adjustment
def step_adjustment(adj, delta): def step_adjustment(adj: Gtk.Adjustment, delta: int) -> None:
v = adj.get_value() + delta v = adj.get_value() + delta
# clamp to the allowed range # clamp to the allowed range
v = max(v, int(adj.get_lower())) v = max(v, int(adj.get_lower()))
@ -298,36 +299,43 @@ def step_adjustment(adj, delta):
adj.set_value(v) adj.set_value(v)
# masks used to indicate the presence of particular line endings def _get_default_lang() -> Optional[str]:
DOS_FORMAT = 1 lang = locale.getdefaultlocale()[0]
MAC_FORMAT = 2 if isWindows():
UNIX_FORMAT = 4 # gettext looks for the language using environment variables which
# are normally not set on Windows so we try setting it for them
for lang_env in 'LC_ALL', 'LC_CTYPE', 'LANG', 'LANGUAGE':
if lang_env in os.environ:
lang = os.environ[lang_env]
# remove any additional languages, encodings, or modifications
for c in ':.@':
lang = lang.split(c)[0]
break
else:
if lang is not None:
os.environ['LANG'] = lang
return lang
class LineEnding(IntFlag):
'''Enumeration of line endings.
Values can be used as flags in bitwise operations.'''
DOS_FORMAT = 1
MAC_FORMAT = 2
UNIX_FORMAT = 4
# avoid some dictionary lookups when string.whitespace is used in loops # avoid some dictionary lookups when string.whitespace is used in loops
# this is sorted based upon frequency to speed up code for stripping whitespace # this is sorted based upon frequency to speed up code for stripping whitespace
whitespace = ' \t\n\r\x0b\x0c' whitespace: Final[str] = ' \t\n\r\x0b\x0c'
# use the program's location as a starting place to search for supporting files # use the program's location as a starting place to search for supporting files
# such as icon and help documentation # such as icon and help documentation
if hasattr(sys, 'frozen'): app_path: Final[str] = sys.executable if hasattr(sys, 'frozen') else os.path.realpath(sys.argv[0])
app_path = sys.executable bin_dir: Final[str] = os.path.dirname(app_path)
else:
app_path = os.path.realpath(sys.argv[0])
bin_dir = os.path.dirname(app_path)
# translation location: '../share/locale/<LANG>/LC_MESSAGES/diffuse.mo' # translation location: '../share/locale/<LANG>/LC_MESSAGES/diffuse.mo'
# where '<LANG>' is the language key # where '<LANG>' is the language key
lang = locale.getdefaultlocale()[0] lang: Final[Optional[str]] = _get_default_lang()
if isWindows():
# gettext looks for the language using environment variables which
# are normally not set on Windows so we try setting it for them
for lang_env in 'LC_ALL', 'LC_CTYPE', 'LANG', 'LANGUAGE':
if lang_env in os.environ:
lang = os.environ[lang_env]
# remove any additional languages, encodings, or modifications
for c in ':.@':
lang = lang.split(c)[0]
break
else:
if lang is not None:
os.environ['LANG'] = lang

View File

@ -20,6 +20,7 @@
import os import os
from diffuse import utils from diffuse import utils
from diffuse.preferences import Preferences
from diffuse.vcs.folder_set import FolderSet from diffuse.vcs.folder_set import FolderSet
from diffuse.vcs.vcs_interface import VcsInterface from diffuse.vcs.vcs_interface import VcsInterface
@ -61,7 +62,7 @@ class Git(VcsInterface):
# sort the results # sort the results
return [modified[k] for k in sorted(modified.keys())] return [modified[k] for k in sorted(modified.keys())]
def _extractPath(self, s, prefs): def _extractPath(self, s: str, prefs: Preferences) -> str:
return os.path.join(self.root, prefs.convertToNativePath(s.strip())) return os.path.join(self.root, prefs.convertToNativePath(s.strip()))
def getFolderTemplate(self, prefs, names): def getFolderTemplate(self, prefs, names):

View File

@ -19,6 +19,8 @@
import os import os
from typing import Optional
from diffuse import utils from diffuse import utils
from diffuse.vcs.folder_set import FolderSet from diffuse.vcs.folder_set import FolderSet
from diffuse.vcs.vcs_interface import VcsInterface from diffuse.vcs.vcs_interface import VcsInterface
@ -26,9 +28,9 @@ from diffuse.vcs.vcs_interface import VcsInterface
# Mercurial support # Mercurial support
class Hg(VcsInterface): class Hg(VcsInterface):
def __init__(self, root): def __init__(self, root: str):
VcsInterface.__init__(self, root) VcsInterface.__init__(self, root)
self.working_rev = None self.working_rev: Optional[str] = None
def _getPreviousRevision(self, prefs, rev): def _getPreviousRevision(self, prefs, rev):
if rev is None: if rev is None:

View File

@ -25,21 +25,21 @@ from diffuse.vcs.svn import Svn
class Svk(Svn): class Svk(Svn):
@staticmethod @staticmethod
def _getVcs(): def _getVcs() -> str:
return 'svk' return 'svk'
@staticmethod @staticmethod
def _getURLPrefix(): def _getURLPrefix() -> str:
return 'Depot Path: ' return 'Depot Path: '
@staticmethod @staticmethod
def _parseStatusLine(s): def _parseStatusLine(s: str) -> tuple[str, str]:
if len(s) < 4 or s[0] not in 'ACDMR': if len(s) < 4 or s[0] not in 'ACDMR':
return '', '' return '', ''
return s[0], s[4:] return s[0], s[4:]
@staticmethod @staticmethod
def _getPreviousRevision(rev): def _getPreviousRevision(rev: str) -> str:
if rev is None: if rev is None:
return 'HEAD' return 'HEAD'
if rev.endswith('@'): if rev.endswith('@'):

View File

@ -21,6 +21,7 @@ import os
import glob import glob
from gettext import gettext as _ from gettext import gettext as _
from typing import Optional
from diffuse import utils from diffuse import utils
from diffuse.vcs.folder_set import FolderSet from diffuse.vcs.folder_set import FolderSet
@ -30,20 +31,20 @@ from diffuse.vcs.vcs_interface import VcsInterface
# Subversion support # Subversion support
# SVK support subclasses from this # SVK support subclasses from this
class Svn(VcsInterface): class Svn(VcsInterface):
def __init__(self, root): def __init__(self, root: str):
VcsInterface.__init__(self, root) VcsInterface.__init__(self, root)
self.url = None self.url: Optional[str] = None
@staticmethod @staticmethod
def _getVcs(): def _getVcs() -> str:
return 'svn' return 'svn'
@staticmethod @staticmethod
def _getURLPrefix(): def _getURLPrefix() -> str:
return 'URL: ' return 'URL: '
@staticmethod @staticmethod
def _parseStatusLine(s): def _parseStatusLine(s: str) -> tuple[str, str]:
if len(s) < 8 or s[0] not in 'ACDMR': if len(s) < 8 or s[0] not in 'ACDMR':
return '', '' return '', ''
# subversion 1.6 adds a new column # subversion 1.6 adds a new column
@ -53,7 +54,7 @@ class Svn(VcsInterface):
return s[0], s[k:] return s[0], s[k:]
@staticmethod @staticmethod
def _getPreviousRevision(rev): def _getPreviousRevision(rev: str) -> str:
if rev is None: if rev is None:
return 'BASE' return 'BASE'
m = int(rev) m = int(rev)

View File

@ -20,7 +20,7 @@
class VcsInterface: class VcsInterface:
"""Interface for the VCSs.""" """Interface for the VCSs."""
def __init__(self, root): def __init__(self, root: str):
"""The object will initialized with the repository's root folder.""" """The object will initialized with the repository's root folder."""
self.root = root self.root = root

View File

@ -20,9 +20,12 @@
import os import os
from gettext import gettext as _ from gettext import gettext as _
from typing import Optional
from diffuse import utils from diffuse import utils
from diffuse.preferences import Preferences
from diffuse.vcs.folder_set import FolderSet from diffuse.vcs.folder_set import FolderSet
from diffuse.vcs.vcs_interface import VcsInterface
from diffuse.vcs.bzr import Bzr from diffuse.vcs.bzr import Bzr
from diffuse.vcs.cvs import Cvs from diffuse.vcs.cvs import Cvs
from diffuse.vcs.darcs import Darcs from diffuse.vcs.darcs import Darcs
@ -35,7 +38,7 @@ from diffuse.vcs.svn import Svn
class VcsRegistry: class VcsRegistry:
def __init__(self): def __init__(self) -> None:
# initialise the VCS objects # initialise the VCS objects
self._get_repo = { self._get_repo = {
'bzr': _get_bzr_repo, 'bzr': _get_bzr_repo,
@ -50,7 +53,7 @@ class VcsRegistry:
} }
# determines which VCS to use for files in the named folder # determines which VCS to use for files in the named folder
def findByFolder(self, path, prefs): def findByFolder(self, path: str, prefs: Preferences) -> Optional[VcsInterface]:
path = os.path.abspath(path) path = os.path.abspath(path)
for vcs in prefs.getString('vcs_search_order').split(): for vcs in prefs.getString('vcs_search_order').split():
if vcs in self._get_repo: if vcs in self._get_repo:
@ -60,14 +63,14 @@ class VcsRegistry:
return None return None
# determines which VCS to use for the named file # determines which VCS to use for the named file
def findByFilename(self, name, prefs): def findByFilename(self, name: str, prefs: Preferences) -> Optional[VcsInterface]:
if name is not None: if name is not None:
return self.findByFolder(os.path.dirname(name), prefs) return self.findByFolder(os.path.dirname(name), prefs)
return None return None
# utility method to help find folders used by version control systems # utility method to help find folders used by version control systems
def _find_parent_dir_with(path, dir_name): def _find_parent_dir_with(path: str, dir_name: str) -> Optional[str]:
while True: while True:
name = os.path.join(path, dir_name) name = os.path.join(path, dir_name)
if os.path.isdir(name): if os.path.isdir(name):
@ -76,28 +79,28 @@ def _find_parent_dir_with(path, dir_name):
if newpath == path: if newpath == path:
break break
path = newpath path = newpath
return None
def _get_bzr_repo(path, prefs): def _get_bzr_repo(path: str, prefs: Preferences) -> Optional[VcsInterface]:
p = _find_parent_dir_with(path, '.bzr') p = _find_parent_dir_with(path, '.bzr')
return Bzr(p) if p else None return Bzr(p) if p else None
def _get_cvs_repo(path, prefs): def _get_cvs_repo(path: str, prefs: Preferences) -> Optional[VcsInterface]:
return Cvs(path) if os.path.isdir(os.path.join(path, 'CVS')) else None return Cvs(path) if os.path.isdir(os.path.join(path, 'CVS')) else None
def _get_darcs_repo(path, prefs): def _get_darcs_repo(path: str, prefs: Preferences) -> Optional[VcsInterface]:
p = _find_parent_dir_with(path, '_darcs') p = _find_parent_dir_with(path, '_darcs')
return Darcs(p) if p else None return Darcs(p) if p else None
def _get_git_repo(path, prefs): def _get_git_repo(path: str, prefs: Preferences) -> Optional[VcsInterface]:
if 'GIT_DIR' in os.environ: if 'GIT_DIR' in os.environ:
try: try:
d = path ss: list[str] = utils.popenReadLines(
ss = utils.popenReadLines( path,
d,
[ [
prefs.getString('git_bin'), prefs.getString('git_bin'),
'rev-parse', 'rev-parse',
@ -107,20 +110,20 @@ def _get_git_repo(path, prefs):
'git_bash') 'git_bash')
if len(ss) > 0: if len(ss) > 0:
# be careful to handle trailing slashes # be careful to handle trailing slashes
d = d.split(os.sep) dirs = path.split(os.sep)
if d[-1] != '': if dirs[-1] != '':
d.append('') dirs.append('')
ss = utils.strip_eol(ss[0]).split('/') ss = utils.strip_eol(ss[0]).split('/')
if ss[-1] != '': if ss[-1] != '':
ss.append('') ss.append('')
n = len(ss) n = len(ss)
if n <= len(d): if n <= len(dirs):
del d[-n:] del dirs[-n:]
if len(d) == 0: if len(dirs) == 0:
d = os.curdir path = os.curdir
else: else:
d = os.sep.join(d) path = os.sep.join(dirs)
return Git(d) return Git(path)
except (IOError, OSError): except (IOError, OSError):
# working tree not found # working tree not found
pass pass
@ -133,19 +136,20 @@ def _get_git_repo(path, prefs):
if newpath == path: if newpath == path:
break break
path = newpath path = newpath
return None
def _get_hg_repo(path, prefs): def _get_hg_repo(path: str, prefs: Preferences) -> Optional[VcsInterface]:
p = _find_parent_dir_with(path, '.hg') p = _find_parent_dir_with(path, '.hg')
return Hg(p) if p else None return Hg(p) if p else None
def _get_mtn_repo(path, prefs): def _get_mtn_repo(path: str, prefs: Preferences) -> Optional[VcsInterface]:
p = _find_parent_dir_with(path, '_MTN') p = _find_parent_dir_with(path, '_MTN')
return Mtn(p) if p else None return Mtn(p) if p else None
def _get_rcs_repo(path, prefs): def _get_rcs_repo(path: str, prefs: Preferences) -> Optional[VcsInterface]:
if os.path.isdir(os.path.join(path, 'RCS')): if os.path.isdir(os.path.join(path, 'RCS')):
return Rcs(path) return Rcs(path)
@ -162,12 +166,12 @@ def _get_rcs_repo(path, prefs):
return None return None
def _get_svn_repo(path, prefs): def _get_svn_repo(path: str, prefs: Preferences) -> Optional[VcsInterface]:
p = _find_parent_dir_with(path, '.svn') p = _find_parent_dir_with(path, '.svn')
return Svn(p) if p else None return Svn(p) if p else None
def _get_svk_repo(path, prefs): def _get_svk_repo(path: str, prefs: Preferences) -> Optional[VcsInterface]:
name = path name = path
# parse the ~/.svk/config file to discover which directories are part of # parse the ~/.svk/config file to discover which directories are part of
# SVK repositories # SVK repositories
@ -181,7 +185,7 @@ def _get_svk_repo(path, prefs):
try: try:
# find working copies by parsing the config file # find working copies by parsing the config file
with open(svkconfig, 'r', encoding='utf-8') as f: with open(svkconfig, 'r', encoding='utf-8') as f:
ss = utils.readlines(f) ss: list[str] = utils.readlines(f)
projs, sep = [], os.sep projs, sep = [], os.sep
# find the separator character # find the separator character
for s in ss: for s in ss:

View File

@ -22,10 +22,11 @@ import os
import unicodedata import unicodedata
from gettext import gettext as _ from gettext import gettext as _
from typing import Dict from typing import Any, Dict, List
from diffuse import utils from diffuse import utils
from diffuse.resources import theResources from diffuse.resources import theResources
from diffuse.utils import LineEnding
import gi # type: ignore import gi # type: ignore
gi.require_version('GObject', '2.0') gi.require_version('GObject', '2.0')
@ -148,9 +149,9 @@ class ScrolledWindow(Gtk.Grid):
class FileDiffViewerBase(Gtk.Grid): class FileDiffViewerBase(Gtk.Grid):
# class describing a text pane # class describing a text pane
class Pane: class Pane:
def __init__(self): def __init__(self) -> None:
# list of lines displayed in this pane (including spacing lines) # list of lines displayed in this pane (including spacing lines)
self.lines = [] self.lines: List[FileDiffViewerBase.Line] = []
# high water mark for line length in Pango units (used to determine # high water mark for line length in Pango units (used to determine
# the required horizontal scroll range) # the required horizontal scroll range)
self.line_lengths = 0 self.line_lengths = 0
@ -160,11 +161,11 @@ class FileDiffViewerBase(Gtk.Grid):
# self.syntax_cache[i] corresponds to self.lines[i] # self.syntax_cache[i] corresponds to self.lines[i]
# the list is truncated when a change to a line invalidates a # the list is truncated when a change to a line invalidates a
# portion of the cache # portion of the cache
self.syntax_cache = [] self.syntax_cache: List[List[Any]] = []
# cache of character differences for each line # cache of character differences for each line
# self.diff_cache[i] corresponds to self.lines[i] # self.diff_cache[i] corresponds to self.lines[i]
# portion of the cache are cleared by setting entries to None # portion of the cache are cleared by setting entries to None
self.diff_cache = [] self.diff_cache: List[List[Any]] = []
# mask indicating the type of line endings present # mask indicating the type of line endings present
self.format = 0 self.format = 0
# number of lines with edits # number of lines with edits
@ -314,7 +315,7 @@ class FileDiffViewerBase(Gtk.Grid):
# create panes # create panes
self.dareas = [] self.dareas = []
self.panes = [] self.panes: List[FileDiffViewerBase.Pane] = []
self.hadj = Gtk.Adjustment.new(0, 0, 0, 0, 0, 0) self.hadj = Gtk.Adjustment.new(0, 0, 0, 0, 0, 0)
self.vadj = Gtk.Adjustment.new(0, 0, 0, 0, 0, 0) self.vadj = Gtk.Adjustment.new(0, 0, 0, 0, 0, 0)
for i in range(n): for i in range(n):
@ -3539,15 +3540,15 @@ class FileDiffViewerBase(Gtk.Grid):
# 'convert_to_dos' action # 'convert_to_dos' action
def convert_to_dos(self): def convert_to_dos(self):
self.convert_format(utils.DOS_FORMAT) self.convert_format(LineEnding.DOS_FORMAT)
# 'convert_to_mac' action # 'convert_to_mac' action
def convert_to_mac(self): def convert_to_mac(self):
self.convert_format(utils.MAC_FORMAT) self.convert_format(LineEnding.MAC_FORMAT)
# 'convert_to_unix' action # 'convert_to_unix' action
def convert_to_unix(self): def convert_to_unix(self):
self.convert_format(utils.UNIX_FORMAT) self.convert_format(LineEnding.UNIX_FORMAT)
# copies the selected range of lines from pane 'f_src' to 'f_dst' # copies the selected range of lines from pane 'f_src' to 'f_dst'
def merge_lines(self, f_dst, f_src): def merge_lines(self, f_dst, f_src):
@ -3985,11 +3986,11 @@ def _get_format(ss):
for s in ss: for s in ss:
if s is not None: if s is not None:
if _has_dos_line_ending(s): if _has_dos_line_ending(s):
flags |= utils.DOS_FORMAT flags |= LineEnding.DOS_FORMAT
elif _has_mac_line_ending(s): elif _has_mac_line_ending(s):
flags |= utils.MAC_FORMAT flags |= LineEnding.MAC_FORMAT
elif _has_unix_line_ending(s): elif _has_unix_line_ending(s):
flags |= utils.UNIX_FORMAT flags |= LineEnding.UNIX_FORMAT
return flags return flags
@ -4000,17 +4001,17 @@ def _convert_to_format(s, fmt):
if old_format != 0 and (old_format & fmt) == 0: if old_format != 0 and (old_format & fmt) == 0:
s = utils.strip_eol(s) s = utils.strip_eol(s)
# prefer the host line ending style # prefer the host line ending style
if (fmt & utils.DOS_FORMAT) and os.linesep == '\r\n': if (fmt & LineEnding.DOS_FORMAT) and os.linesep == '\r\n':
s += os.linesep s += os.linesep
elif (fmt & utils.MAC_FORMAT) and os.linesep == '\r': elif (fmt & LineEnding.MAC_FORMAT) and os.linesep == '\r':
s += os.linesep s += os.linesep
elif (fmt & utils.UNIX_FORMAT) and os.linesep == '\n': elif (fmt & LineEnding.UNIX_FORMAT) and os.linesep == '\n':
s += os.linesep s += os.linesep
elif fmt & utils.UNIX_FORMAT: elif fmt & LineEnding.UNIX_FORMAT:
s += '\n' s += '\n'
elif fmt & utils.DOS_FORMAT: elif fmt & LineEnding.DOS_FORMAT:
s += '\r\n' s += '\r\n'
elif fmt & utils.MAC_FORMAT: elif fmt & LineEnding.MAC_FORMAT:
s += '\r' s += '\r'
return s return s