diff --git a/.mypy.ini b/.mypy.ini index b9a22a7..39462be 100644 --- a/.mypy.ini +++ b/.mypy.ini @@ -1,2 +1,3 @@ [mypy] warn_unused_ignores = True +disallow_incomplete_defs = True diff --git a/CHANGELOG.md b/CHANGELOG.md index 59ed443..6f33b9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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" - Linters can be run sooner (before installation) - Better messages when an error occurs while parsing the config file +- Start converting the code to static types ### Fixed - Removed the lasting lint errors (i.e. in main.py) diff --git a/src/diffuse/constants.py b/src/diffuse/constants.py index cb01b40..8e13527 100644 --- a/src/diffuse/constants.py +++ b/src/diffuse/constants.py @@ -18,11 +18,12 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. from gettext import gettext as _ +from typing import Final -APP_NAME = 'Diffuse' -COPYRIGHT = '''{copyright} © 2006-2019 Derrick Moser +APP_NAME: Final[str] = 'Diffuse' +COPYRIGHT: Final[str] = '''{copyright} © 2006-2019 Derrick Moser {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() -VERSION = '0.0.0' +VERSION: str = '0.0.0' diff --git a/src/diffuse/dialogs.py b/src/diffuse/dialogs.py index 7b8a030..6f5cc94 100644 --- a/src/diffuse/dialogs.py +++ b/src/diffuse/dialogs.py @@ -32,7 +32,7 @@ from gi.repository import GObject, Gtk # type: ignore # noqa: E402 # the about dialog class AboutDialog(Gtk.AboutDialog): - def __init__(self): + def __init__(self) -> None: Gtk.AboutDialog.__init__(self) self.set_logo_icon_name('io.github.mightycreak.Diffuse') self.set_program_name(constants.APP_NAME) @@ -104,13 +104,13 @@ class FileChooserDialog(Gtk.FileChooserDialog): def set_encoding(self, encoding): self.encoding.set_text(encoding) - def get_encoding(self): + def get_encoding(self) -> str: return self.encoding.get_text() - def get_revision(self): + def get_revision(self) -> str: return self.revision.get_text() - def get_filename(self): + def get_filename(self) -> str: # convert from UTF-8 string to unicode return Gtk.FileChooserDialog.get_filename(self) diff --git a/src/diffuse/main.py b/src/diffuse/main.py index 54e7af1..f1f3f3a 100644 --- a/src/diffuse/main.py +++ b/src/diffuse/main.py @@ -26,6 +26,7 @@ import stat import webbrowser from gettext import gettext as _ +from typing import Optional from urllib.parse import urlparse from diffuse import constants @@ -33,6 +34,7 @@ from diffuse import utils from diffuse.dialogs import AboutDialog, FileChooserDialog, NumericDialog, SearchDialog from diffuse.preferences import Preferences from diffuse.resources import theResources +from diffuse.utils import LineEnding from diffuse.vcs.vcs_registry import VcsRegistry from diffuse.widgets import FileDiffViewerBase from diffuse.widgets import createMenu, LINE_MODE, CHAR_MODE, ALIGN_MODE @@ -118,7 +120,7 @@ class Diffuse(Gtk.Window): class FileDiffViewer(FileDiffViewerBase): # pane header class PaneHeader(Gtk.Box): - def __init__(self): + def __init__(self) -> None: Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL, spacing=0) _append_buttons(self, Gtk.IconSize.MENU, [ [Gtk.STOCK_OPEN, self.button_cb, 'open', _('Open File...')], @@ -171,7 +173,7 @@ class Diffuse(Gtk.Window): # pane footer class PaneFooter(Gtk.Box): - def __init__(self): + def __init__(self) -> None: Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL, spacing=0) self.cursor = label = Gtk.Label.new() self.cursor.set_size_request(-1, -1) @@ -212,11 +214,11 @@ class Diffuse(Gtk.Window): # set the format label def setFormat(self, s): v = [] - if s & utils.DOS_FORMAT: + if s & LineEnding.DOS_FORMAT: v.append('DOS') - if s & utils.MAC_FORMAT: + if s & LineEnding.MAC_FORMAT: v.append('Mac') - if s & utils.UNIX_FORMAT: + if s & LineEnding.UNIX_FORMAT: v.append('Unix') self.format.set_text('/'.join(v)) @@ -1108,7 +1110,7 @@ class Diffuse(Gtk.Window): self.quit_cb(widget, data) # convenience method to request confirmation when closing the last tab - def _confirm_tab_close(self): + def _confirm_tab_close(self) -> bool: dialog = utils.MessageDialog( self.get_toplevel(), Gtk.MessageType.WARNING, @@ -1193,7 +1195,7 @@ class Diffuse(Gtk.Window): self.setSyntax(s) # create an empty viewer with 'n' panes - def newFileDiffViewer(self, n): + def newFileDiffViewer(self, n: int) -> FileDiffViewer: self.viewer_count += 1 tabname = _('File Merge %d') % (self.viewer_count, ) tab = NotebookTab(tabname, Gtk.STOCK_FILE) @@ -1338,13 +1340,13 @@ class Diffuse(Gtk.Window): ) # close all tabs without differences - def closeOnSame(self): + def closeOnSame(self) -> None: for i in range(self.notebook.get_n_pages() - 1, -1, -1): if not self.notebook.get_nth_page(i).hasDifferences(): self.notebook.remove_page(i) # returns True if the application can safely quit - def confirmQuit(self): + def confirmQuit(self) -> bool: nb = self.notebook 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 # 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()) # callback for the open file menu item @@ -1570,7 +1572,7 @@ class Diffuse(Gtk.Window): self.getCurrentViewer().go_to_line_cb(widget, data) # notify all viewers of changes to the preferences - def preferences_updated(self): + def preferences_updated(self) -> None: n = self.notebook.get_n_pages() self.notebook.set_show_tabs(self.prefs.getBool('tabs_always_show') or n > 1) for i in range(n): @@ -1712,7 +1714,7 @@ def _append_buttons(box, size, specs): # constructs a full URL for the named file -def _path2url(path, proto='file'): +def _path2url(path: str, proto: str = 'file') -> str: r = [proto, ':///'] s = os.path.abspath(path) i = 0 diff --git a/src/diffuse/preferences.py b/src/diffuse/preferences.py index 7d92766..bc16152 100644 --- a/src/diffuse/preferences.py +++ b/src/diffuse/preferences.py @@ -199,7 +199,7 @@ class Preferences: self.path = path if os.path.isfile(self.path): 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) for j, s in enumerate(ss): try: @@ -342,7 +342,7 @@ class Preferences: if tpl[0] in ['Font', 'Integer']: entry = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 0) if tpl[0] == 'Font': - button = _FontButton() + button = Gtk.FontButton() button.set_font(self.string_prefs[tpl[1]]) else: button = Gtk.SpinButton.new( @@ -368,28 +368,28 @@ class Preferences: return table # get/set methods to manipulate the preference values - def getBool(self, name): + def getBool(self, name: str) -> bool: return self.bool_prefs[name] - def setBool(self, name, value): + def setBool(self, name: str, value: bool) -> None: self.bool_prefs[name] = value - def getInt(self, name): + def getInt(self, name: str) -> int: return self.int_prefs[name] - def getString(self, name): + def getString(self, name: str) -> str: return self.string_prefs[name] - def setString(self, name, value): + def setString(self, name: str, value: str) -> None: self.string_prefs[name] = value def getEncodings(self): return self.encodings - def _getDefaultEncodings(self): + def _getDefaultEncodings(self) -> list[str]: return self.string_prefs['encoding_auto_detect_codecs'].split() - def getDefaultEncoding(self): + def getDefaultEncoding(self) -> str: return self.string_prefs['encoding_default_codec'] # 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 # 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: # treat as a cygwin path s = s.replace(os.sep, '/') @@ -436,15 +436,6 @@ class Preferences: 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 class _FileEntry(Gtk.Box): def __init__(self, parent, title): @@ -475,8 +466,8 @@ class _FileEntry(Gtk.Box): self.entry.set_text(dialog.get_filename()) dialog.destroy() - def set_text(self, s): + def set_text(self, s: str) -> None: self.entry.set_text(s) - def get_text(self): + def get_text(self) -> str: return self.entry.get_text() diff --git a/src/diffuse/resources.py b/src/diffuse/resources.py index 8470f2e..ceb14e8 100644 --- a/src/diffuse/resources.py +++ b/src/diffuse/resources.py @@ -31,6 +31,7 @@ import shlex from distutils import util from gettext import gettext as _ +from typing import Final from diffuse import utils @@ -529,7 +530,7 @@ class Resources: # colour resources 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] self.red = r self.green = g @@ -601,4 +602,4 @@ class _SyntaxParser: return state_name, blocks -theResources = Resources() +theResources: Final[Resources] = Resources() diff --git a/src/diffuse/utils.py b/src/diffuse/utils.py index 77ba3c3..95c9b9c 100644 --- a/src/diffuse/utils.py +++ b/src/diffuse/utils.py @@ -23,7 +23,9 @@ import locale import subprocess import traceback +from enum import IntFlag from gettext import gettext as _ +from typing import Final, Optional, TextIO from diffuse import constants from diffuse.resources import theResources @@ -74,38 +76,38 @@ class EncodingMenu(Gtk.Box): return self.encodings[i] if i >= 0 else None -# platform test -def isWindows(): +def isWindows() -> bool: + '''Returns true if OS is Windows; otherwise false.''' return os.name == 'nt' -def _logPrintOutput(msg): +def _logPrintOutput(msg: str) -> None: if theResources.getOptionAsBool('log_print_output'): print(msg, file=sys.stderr) if theResources.getOptionAsBool('log_print_stack'): traceback.print_stack() -# convenience function to display debug messages -def logDebug(msg): +def logDebug(msg: str) -> None: + '''Report debug message.''' _logPrintOutput(f'DEBUG: {msg}') -# report error messages -def logError(msg): +def logError(msg: str) -> None: + '''Report error message.''' _logPrintOutput(f'ERROR: {msg}') -# report error messages and show dialog -def logErrorAndDialog(msg, parent=None): +def logErrorAndDialog(msg: str, parent: Gtk.Widget = None) -> None: + '''Report error message and show dialog.''' logError(msg) dialog = MessageDialog(parent, Gtk.MessageType.ERROR, msg) dialog.run() dialog.destroy() -# create nested subdirectories and return the complete path -def make_subdirs(p, ss): +def make_subdirs(p: str, ss: list[str]) -> str: + '''Create nested subdirectories and return the complete path.''' for s in ss: p = os.path.join(p, s) if not os.path.exists(p): @@ -116,16 +118,16 @@ def make_subdirs(p, ss): return p -# returns the Windows drive or share from a from an absolute path -def _drive_from_path(path): +def _drive_from_path(path: str) -> str: + '''Returns the Windows drive or share from a from an absolute path.''' d = path.split(os.sep) 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] -# constructs a relative path from 'a' to 'b', both should be absolute paths -def relpath(a, b): +def relpath(a: str, b: str) -> str: + '''Constructs a relative path from 'a' to 'b', both should be absolute paths.''' if isWindows(): if _drive_from_path(a) != _drive_from_path(b): return b @@ -151,12 +153,12 @@ def safeRelativePath(abspath1, name, prefs, cygwin_pref): return s -# escape arguments for use with bash -def _bash_escape(s): +def _bash_escape(s: str) -> str: + '''Escape arguments for use with bash.''' return "'" + s.replace("'", "'\\''") + "'" -def _use_flatpak(): +def _use_flatpak() -> bool: return theResources.getOptionAsBool('use_flatpak') @@ -201,9 +203,8 @@ def popenRead(dn, cmd, prefs, bash_pref, success_results=None): return s -# returns the number of characters in the string excluding any line ending -# characters -def len_minus_line_ending(s): +def len_minus_line_ending(s: str) -> int: + '''Returns the number of characters in the string excluding any line ending characters.''' if s is None: return 0 n = len(s) @@ -214,15 +215,15 @@ def len_minus_line_ending(s): return n -# returns the string without the line ending characters -def strip_eol(s): +def strip_eol(s: str) -> str: + '''Returns the string without the line ending characters.''' if s: s = s[:len_minus_line_ending(s)] return s -# returns the list of strings without line ending characters -def _strip_eols(ss): +def _strip_eols(ss: list[str]) -> list[str]: + '''Returns the list of strings without line ending characters.''' 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'))) -def readconfiglines(fd): +def readconfiglines(fd: TextIO) -> list[str]: return fd.read().replace('\r', '').split('\n') -# escape special glob characters -def globEscape(s): +def globEscape(s: str) -> str: + '''Escape special glob characters.''' m = {c: f'[{c}]' for c in '[]?*'} 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 -def readlines(fd): +def readlines(fd: TextIO) -> list[str]: return _strip_eols(splitlines(fd.read())) -# map an encoding name to its standard form -def norm_encoding(e): +def norm_encoding(e: Optional[str]) -> Optional[str]: + '''Map an encoding name to its standard form.''' if e is not None: return e.replace('-', '_').lower() return None -def null_to_empty(s): +def null_to_empty(s: Optional[str]) -> str: if s is None: s = '' return s # 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 # clamp to the allowed range v = max(v, int(adj.get_lower())) @@ -298,36 +299,43 @@ def step_adjustment(adj, delta): adj.set_value(v) -# masks used to indicate the presence of particular line endings -DOS_FORMAT = 1 -MAC_FORMAT = 2 -UNIX_FORMAT = 4 +def _get_default_lang() -> Optional[str]: + lang = locale.getdefaultlocale()[0] + 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 + 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 # 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 # such as icon and help documentation -if hasattr(sys, 'frozen'): - app_path = sys.executable -else: - app_path = os.path.realpath(sys.argv[0]) -bin_dir = os.path.dirname(app_path) +app_path: Final[str] = sys.executable if hasattr(sys, 'frozen') else os.path.realpath(sys.argv[0]) +bin_dir: Final[str] = os.path.dirname(app_path) # translation location: '../share/locale//LC_MESSAGES/diffuse.mo' # where '' is the language key -lang = locale.getdefaultlocale()[0] -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 +lang: Final[Optional[str]] = _get_default_lang() diff --git a/src/diffuse/vcs/git.py b/src/diffuse/vcs/git.py index 7b1b915..20b2d5f 100644 --- a/src/diffuse/vcs/git.py +++ b/src/diffuse/vcs/git.py @@ -20,6 +20,7 @@ import os from diffuse import utils +from diffuse.preferences import Preferences from diffuse.vcs.folder_set import FolderSet from diffuse.vcs.vcs_interface import VcsInterface @@ -61,7 +62,7 @@ class Git(VcsInterface): # sort the results 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())) def getFolderTemplate(self, prefs, names): diff --git a/src/diffuse/vcs/hg.py b/src/diffuse/vcs/hg.py index 8a2d378..6dc540e 100644 --- a/src/diffuse/vcs/hg.py +++ b/src/diffuse/vcs/hg.py @@ -19,6 +19,8 @@ import os +from typing import Optional + from diffuse import utils from diffuse.vcs.folder_set import FolderSet from diffuse.vcs.vcs_interface import VcsInterface @@ -26,9 +28,9 @@ from diffuse.vcs.vcs_interface import VcsInterface # Mercurial support class Hg(VcsInterface): - def __init__(self, root): + def __init__(self, root: str): VcsInterface.__init__(self, root) - self.working_rev = None + self.working_rev: Optional[str] = None def _getPreviousRevision(self, prefs, rev): if rev is None: diff --git a/src/diffuse/vcs/svk.py b/src/diffuse/vcs/svk.py index 03ed959..bb3f670 100644 --- a/src/diffuse/vcs/svk.py +++ b/src/diffuse/vcs/svk.py @@ -25,21 +25,21 @@ from diffuse.vcs.svn import Svn class Svk(Svn): @staticmethod - def _getVcs(): + def _getVcs() -> str: return 'svk' @staticmethod - def _getURLPrefix(): + def _getURLPrefix() -> str: return 'Depot Path: ' @staticmethod - def _parseStatusLine(s): + def _parseStatusLine(s: str) -> tuple[str, str]: if len(s) < 4 or s[0] not in 'ACDMR': return '', '' return s[0], s[4:] @staticmethod - def _getPreviousRevision(rev): + def _getPreviousRevision(rev: str) -> str: if rev is None: return 'HEAD' if rev.endswith('@'): diff --git a/src/diffuse/vcs/svn.py b/src/diffuse/vcs/svn.py index 2db5390..45cfda2 100644 --- a/src/diffuse/vcs/svn.py +++ b/src/diffuse/vcs/svn.py @@ -21,6 +21,7 @@ import os import glob from gettext import gettext as _ +from typing import Optional from diffuse import utils from diffuse.vcs.folder_set import FolderSet @@ -30,20 +31,20 @@ from diffuse.vcs.vcs_interface import VcsInterface # Subversion support # SVK support subclasses from this class Svn(VcsInterface): - def __init__(self, root): + def __init__(self, root: str): VcsInterface.__init__(self, root) - self.url = None + self.url: Optional[str] = None @staticmethod - def _getVcs(): + def _getVcs() -> str: return 'svn' @staticmethod - def _getURLPrefix(): + def _getURLPrefix() -> str: return 'URL: ' @staticmethod - def _parseStatusLine(s): + def _parseStatusLine(s: str) -> tuple[str, str]: if len(s) < 8 or s[0] not in 'ACDMR': return '', '' # subversion 1.6 adds a new column @@ -53,7 +54,7 @@ class Svn(VcsInterface): return s[0], s[k:] @staticmethod - def _getPreviousRevision(rev): + def _getPreviousRevision(rev: str) -> str: if rev is None: return 'BASE' m = int(rev) diff --git a/src/diffuse/vcs/vcs_interface.py b/src/diffuse/vcs/vcs_interface.py index c2b2b15..63800e8 100644 --- a/src/diffuse/vcs/vcs_interface.py +++ b/src/diffuse/vcs/vcs_interface.py @@ -20,7 +20,7 @@ class VcsInterface: """Interface for the VCSs.""" - def __init__(self, root): + def __init__(self, root: str): """The object will initialized with the repository's root folder.""" self.root = root diff --git a/src/diffuse/vcs/vcs_registry.py b/src/diffuse/vcs/vcs_registry.py index 6e330c2..7232bbe 100644 --- a/src/diffuse/vcs/vcs_registry.py +++ b/src/diffuse/vcs/vcs_registry.py @@ -20,9 +20,12 @@ import os from gettext import gettext as _ +from typing import Optional from diffuse import utils +from diffuse.preferences import Preferences from diffuse.vcs.folder_set import FolderSet +from diffuse.vcs.vcs_interface import VcsInterface from diffuse.vcs.bzr import Bzr from diffuse.vcs.cvs import Cvs from diffuse.vcs.darcs import Darcs @@ -35,7 +38,7 @@ from diffuse.vcs.svn import Svn class VcsRegistry: - def __init__(self): + def __init__(self) -> None: # initialise the VCS objects self._get_repo = { 'bzr': _get_bzr_repo, @@ -50,7 +53,7 @@ class VcsRegistry: } # 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) for vcs in prefs.getString('vcs_search_order').split(): if vcs in self._get_repo: @@ -60,14 +63,14 @@ class VcsRegistry: return None # 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: return self.findByFolder(os.path.dirname(name), prefs) return None # 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: name = os.path.join(path, dir_name) if os.path.isdir(name): @@ -76,28 +79,28 @@ def _find_parent_dir_with(path, dir_name): if newpath == path: break 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') 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 -def _get_darcs_repo(path, prefs): +def _get_darcs_repo(path: str, prefs: Preferences) -> Optional[VcsInterface]: p = _find_parent_dir_with(path, '_darcs') 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: try: - d = path - ss = utils.popenReadLines( - d, + ss: list[str] = utils.popenReadLines( + path, [ prefs.getString('git_bin'), 'rev-parse', @@ -107,20 +110,20 @@ def _get_git_repo(path, prefs): 'git_bash') if len(ss) > 0: # be careful to handle trailing slashes - d = d.split(os.sep) - if d[-1] != '': - d.append('') + dirs = path.split(os.sep) + if dirs[-1] != '': + dirs.append('') ss = utils.strip_eol(ss[0]).split('/') if ss[-1] != '': ss.append('') n = len(ss) - if n <= len(d): - del d[-n:] - if len(d) == 0: - d = os.curdir + if n <= len(dirs): + del dirs[-n:] + if len(dirs) == 0: + path = os.curdir else: - d = os.sep.join(d) - return Git(d) + path = os.sep.join(dirs) + return Git(path) except (IOError, OSError): # working tree not found pass @@ -133,19 +136,20 @@ def _get_git_repo(path, prefs): if newpath == path: break 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') 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') 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')): return Rcs(path) @@ -162,12 +166,12 @@ def _get_rcs_repo(path, prefs): 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') 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 # parse the ~/.svk/config file to discover which directories are part of # SVK repositories @@ -181,7 +185,7 @@ def _get_svk_repo(path, prefs): try: # find working copies by parsing the config file with open(svkconfig, 'r', encoding='utf-8') as f: - ss = utils.readlines(f) + ss: list[str] = utils.readlines(f) projs, sep = [], os.sep # find the separator character for s in ss: diff --git a/src/diffuse/widgets.py b/src/diffuse/widgets.py index 95c4a77..a57a9bb 100644 --- a/src/diffuse/widgets.py +++ b/src/diffuse/widgets.py @@ -22,10 +22,11 @@ import os import unicodedata from gettext import gettext as _ -from typing import Dict +from typing import Any, Dict, List from diffuse import utils from diffuse.resources import theResources +from diffuse.utils import LineEnding import gi # type: ignore gi.require_version('GObject', '2.0') @@ -148,9 +149,9 @@ class ScrolledWindow(Gtk.Grid): class FileDiffViewerBase(Gtk.Grid): # class describing a text pane class Pane: - def __init__(self): + def __init__(self) -> None: # 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 # the required horizontal scroll range) self.line_lengths = 0 @@ -160,11 +161,11 @@ class FileDiffViewerBase(Gtk.Grid): # self.syntax_cache[i] corresponds to self.lines[i] # the list is truncated when a change to a line invalidates a # portion of the cache - self.syntax_cache = [] + self.syntax_cache: List[List[Any]] = [] # cache of character differences for each line # self.diff_cache[i] corresponds to self.lines[i] # 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 self.format = 0 # number of lines with edits @@ -314,7 +315,7 @@ class FileDiffViewerBase(Gtk.Grid): # create panes self.dareas = [] - self.panes = [] + self.panes: List[FileDiffViewerBase.Pane] = [] self.hadj = 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): @@ -3539,15 +3540,15 @@ class FileDiffViewerBase(Gtk.Grid): # 'convert_to_dos' action def convert_to_dos(self): - self.convert_format(utils.DOS_FORMAT) + self.convert_format(LineEnding.DOS_FORMAT) # 'convert_to_mac' action def convert_to_mac(self): - self.convert_format(utils.MAC_FORMAT) + self.convert_format(LineEnding.MAC_FORMAT) # 'convert_to_unix' action 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' def merge_lines(self, f_dst, f_src): @@ -3985,11 +3986,11 @@ def _get_format(ss): for s in ss: if s is not None: if _has_dos_line_ending(s): - flags |= utils.DOS_FORMAT + flags |= LineEnding.DOS_FORMAT elif _has_mac_line_ending(s): - flags |= utils.MAC_FORMAT + flags |= LineEnding.MAC_FORMAT elif _has_unix_line_ending(s): - flags |= utils.UNIX_FORMAT + flags |= LineEnding.UNIX_FORMAT return flags @@ -4000,17 +4001,17 @@ def _convert_to_format(s, fmt): if old_format != 0 and (old_format & fmt) == 0: s = utils.strip_eol(s) # 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 - elif (fmt & utils.MAC_FORMAT) and os.linesep == '\r': + elif (fmt & LineEnding.MAC_FORMAT) and os.linesep == '\r': s += os.linesep - elif (fmt & utils.UNIX_FORMAT) and os.linesep == '\n': + elif (fmt & LineEnding.UNIX_FORMAT) and os.linesep == '\n': s += os.linesep - elif fmt & utils.UNIX_FORMAT: + elif fmt & LineEnding.UNIX_FORMAT: s += '\n' - elif fmt & utils.DOS_FORMAT: + elif fmt & LineEnding.DOS_FORMAT: s += '\r\n' - elif fmt & utils.MAC_FORMAT: + elif fmt & LineEnding.MAC_FORMAT: s += '\r' return s