diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b0e303a..9a77a65 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,7 +23,7 @@ jobs: - name: Pylint uses: cclauss/GitHub-Action-for-pylint@master with: - args: "pylint src/vcs/ src/dialogs.py src/preferences.py src/resources.py src/utils.py src/widgets.py" + args: "apk add --no-cache gtk+3.0-dev gobject-introspection-dev ; pip install -r requirements.txt ; pylint src/**/*.py" meson-build-test: runs-on: ubuntu-latest diff --git a/.pylintrc b/.pylintrc index 1e8baea..c78fdbb 100644 --- a/.pylintrc +++ b/.pylintrc @@ -24,7 +24,7 @@ ignore=CVS # Add files or directories matching the regex patterns to the ignore-list. The # regex matches against paths. -ignore-paths= +ignore-paths=src/diffuse/main.py # Files or directories matching the regex patterns are skipped. The regex # matches against base names, not paths. @@ -90,12 +90,10 @@ disable=raw-checker-failed, # temporary silenced messages (ordered alphabetically) duplicate-code, fixme, - import-error, invalid-name, missing-class-docstring, missing-function-docstring, missing-module-docstring, - no-self-use, too-few-public-methods, too-many-arguments, too-many-branches, diff --git a/CHANGELOG.md b/CHANGELOG.md index b632692..79a560a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The new widgets.py is a bit fat though (~4000 lines) ### Fixed +- The intense code cleaning seems to have fixed a bug with the `-c` argument + (#120) ## [0.7.2] - 2021-11-18 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..95fc636 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +PyGObject~=3.40 +pylint~=2.11 diff --git a/src/__init__.py b/src/diffuse/__init__.py similarity index 100% rename from src/__init__.py rename to src/diffuse/__init__.py diff --git a/src/constants.py.in b/src/diffuse/constants.py.in similarity index 100% rename from src/constants.py.in rename to src/diffuse/constants.py.in diff --git a/src/dialogs.py b/src/diffuse/dialogs.py similarity index 97% rename from src/dialogs.py rename to src/diffuse/dialogs.py index 13f2934..4701014 100644 --- a/src/dialogs.py +++ b/src/diffuse/dialogs.py @@ -26,7 +26,9 @@ gi.require_version('Gtk', '3.0') from gi.repository import GObject, Gtk # pylint: enable=wrong-import-position +# pylint: disable-next=no-name-in-module from diffuse import constants + from diffuse import utils # the about dialog @@ -67,7 +69,8 @@ class FileChooserDialog(Gtk.FileChooserDialog): # location for empty panes last_chosen_folder = os.path.realpath(os.curdir) - def __current_folder_changed_cb(self, widget): + @staticmethod + def _current_folder_changed_cb(widget): FileChooserDialog.last_chosen_folder = widget.get_current_folder() def __init__(self, title, parent, prefs, action, accept, rev=False): @@ -96,7 +99,7 @@ class FileChooserDialog(Gtk.FileChooserDialog): self.vbox.pack_start(hbox, False, False, 0) # pylint: disable=no-member hbox.show() self.set_current_folder(self.last_chosen_folder) - self.connect('current-folder-changed', self.__current_folder_changed_cb) + self.connect('current-folder-changed', self._current_folder_changed_cb) def set_encoding(self, encoding): self.encoding.set_text(encoding) @@ -107,6 +110,7 @@ class FileChooserDialog(Gtk.FileChooserDialog): def get_revision(self): return self.revision.get_text() + # pylint: disable-next=arguments-differ def get_filename(self): # convert from UTF-8 string to unicode return Gtk.FileChooserDialog.get_filename(self) # pylint: disable=no-member diff --git a/src/diffuse.in b/src/diffuse/diffuse.in similarity index 100% rename from src/diffuse.in rename to src/diffuse/diffuse.in diff --git a/src/main.py b/src/diffuse/main.py similarity index 99% rename from src/main.py rename to src/diffuse/main.py index c02ba62..995e6df 100644 --- a/src/main.py +++ b/src/diffuse/main.py @@ -20,11 +20,9 @@ import os import sys import codecs -import difflib import encodings import shlex import stat -import unicodedata import webbrowser # pylint: disable=wrong-import-position @@ -40,14 +38,16 @@ from gi.repository import GObject, Gtk, Gdk, GdkPixbuf, Pango, PangoCairo from urllib.parse import urlparse +# pylint: disable-next=no-name-in-module from diffuse import constants + from diffuse import utils from diffuse.dialogs import AboutDialog, FileChooserDialog, NumericDialog, SearchDialog from diffuse.preferences import Preferences -from diffuse.resources import Resources, theResources +from diffuse.resources import theResources from diffuse.vcs.vcs_registry import VcsRegistry -from diffuse.widgets import FileDiffViewer, ScrolledWindow -from diffuse.widgets import LINE_MODE, CHAR_MODE, ALIGN_MODE +from diffuse.widgets import FileDiffViewer +from diffuse.widgets import createMenu, LINE_MODE, CHAR_MODE, ALIGN_MODE theVCSs = VcsRegistry() @@ -933,11 +933,11 @@ class Diffuse(Gtk.Window): utils.logDebug(f'Error writing {statepath}.') # select viewer for a newly selected file in the confirm close dialogue - def __confirmClose_row_activated_cb(self, tree, path, col, model): + def _confirmClose_row_activated_cb(self, tree, path, col, model): self.notebook.set_current_page(self.notebook.page_num(model[path][3])) # toggle save state for a file listed in the confirm close dialogue - def __confirmClose_toggle_cb(self, cell, path, model): + def _confirmClose_toggle_cb(self, cell, path, model): model[path][0] = not model[path][0] # returns True if the list of viewers can be closed. The user will be @@ -970,7 +970,7 @@ class Diffuse(Gtk.Window): sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) treeview = Gtk.TreeView.new_with_model(model) r = Gtk.CellRendererToggle.new() - r.connect('toggled', self.__confirmClose_toggle_cb, model) + r.connect('toggled', self._confirmClose_toggle_cb, model) column = Gtk.TreeViewColumn(None, r) column.add_attribute(r, 'active', 0) treeview.append_column(column) @@ -984,7 +984,7 @@ class Diffuse(Gtk.Window): column.set_resizable(True) column.set_sort_column_id(2) treeview.append_column(column) - treeview.connect('row-activated', self.__confirmClose_row_activated_cb, model) + treeview.connect('row-activated', self._confirmClose_row_activated_cb, model) sw.add(treeview) treeview.show() dialog.vbox.pack_start(sw, True, True, 0) # pylint: disable=no-member @@ -1190,7 +1190,7 @@ class Diffuse(Gtk.Window): new_items = [] for item in items: name, data = item - # get full path to an existing ancessor directory + # get full path to an existing ancestor directory dn = os.path.abspath(name) while not os.path.isdir(dn): dn, old_dn = os.path.dirname(dn), dn @@ -1552,7 +1552,7 @@ def _create_menu_bar(specs, radio, accel_group): menu_bar = Gtk.MenuBar.new() for label, spec in specs: menu = Gtk.MenuItem.new_with_mnemonic(label) - menu.set_submenu(utils.createMenu(spec, radio, accel_group)) + menu.set_submenu(createMenu(spec, radio, accel_group)) menu.set_use_underline(True) menu.show() menu_bar.append(menu) diff --git a/src/diffuse/meson.build b/src/diffuse/meson.build new file mode 100644 index 0000000..b731de1 --- /dev/null +++ b/src/diffuse/meson.build @@ -0,0 +1,46 @@ +pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name()) +moduledir = join_paths(pkgdatadir, 'diffuse') +sysconfdir = join_paths(get_option('prefix'), get_option('sysconfdir')) + +python = import('python') + +conf = configuration_data() +conf.set('PYTHON', python.find_installation('python3').path()) +conf.set('pkgdatadir', pkgdatadir) +conf.set('localedir', join_paths(get_option('prefix'), get_option('localedir'))) + +configure_file( + input: 'diffuse.in', + output: 'diffuse', + configuration: conf, + install: true, + install_dir: get_option('bindir') +) + +conf = configuration_data() +conf.set('VERSION', meson.project_version()) +conf.set('sysconfigdir', sysconfdir) +conf.set('log_print_output', get_option('log_print_output')) +conf.set('log_print_stack', get_option('log_print_stack')) +conf.set('use_flatpak', get_option('use_flatpak')) + +configure_file( + input: 'constants.py.in', + output: 'constants.py', + configuration: conf, + install: true, + install_dir: moduledir +) + +diffuse_sources = [ + '__init__.py', + 'dialogs.py', + 'main.py', + 'preferences.py', + 'resources.py', + 'utils.py', + 'widgets.py', +] + +install_data(diffuse_sources, install_dir: moduledir) +install_subdir('vcs', install_dir: moduledir, strip_directory: false) diff --git a/src/preferences.py b/src/diffuse/preferences.py similarity index 99% rename from src/preferences.py rename to src/diffuse/preferences.py index 197cded..029e6a7 100644 --- a/src/preferences.py +++ b/src/diffuse/preferences.py @@ -29,9 +29,11 @@ gi.require_version('Gtk', '3.0') from gi.repository import Gtk # pylint: enable=wrong-import-position -from diffuse import utils +# pylint: disable-next=no-name-in-module from diffuse import constants +from diffuse import utils + # class to store preferences and construct a dialogue for manipulating them class Preferences: def __init__(self, path): diff --git a/src/resources.py b/src/diffuse/resources.py similarity index 100% rename from src/resources.py rename to src/diffuse/resources.py diff --git a/src/utils.py b/src/diffuse/utils.py similarity index 83% rename from src/utils.py rename to src/diffuse/utils.py index 322cdb2..e104952 100644 --- a/src/utils.py +++ b/src/diffuse/utils.py @@ -29,8 +29,8 @@ gi.require_version('Gtk', '3.0') from gi.repository import Gtk # pylint: enable=wrong-import-position +# pylint: disable-next=no-name-in-module from diffuse import constants -from diffuse.resources import theResources # convenience class for displaying a message dialogue class MessageDialog(Gtk.MessageDialog): @@ -272,50 +272,6 @@ def step_adjustment(adj, delta): v = min(v, int(adj.get_upper() - adj.get_page_size())) adj.set_value(v) -# convenience method for creating a menu according to a template -def createMenu(specs, radio=None, accel_group=None): - menu = Gtk.Menu.new() - for spec in specs: - if len(spec) > 0: - if len(spec) > 7 and spec[7] is not None: - g, k = spec[7] - if g not in radio: - item = Gtk.RadioMenuItem.new_with_mnemonic_from_widget(None, spec[0]) - radio[g] = (item, {}) - else: - item = Gtk.RadioMenuItem.new_with_mnemonic_from_widget(radio[g][0], spec[0]) - radio[g][1][k] = item - else: - item = Gtk.ImageMenuItem.new_with_mnemonic(spec[0]) - cb = spec[1] - if cb is not None: - data = spec[2] - item.connect('activate', cb, data) - if len(spec) > 3 and spec[3] is not None: - image = Gtk.Image.new() - image.set_from_stock(spec[3], Gtk.IconSize.MENU) # pylint: disable=no-member - item.set_image(image) - if accel_group is not None and len(spec) > 4: - a = theResources.getKeyBindings('menu', spec[4]) - if len(a) > 0: - key, modifier = a[0] - item.add_accelerator( - 'activate', - accel_group, - key, - modifier, - Gtk.AccelFlags.VISIBLE) - if len(spec) > 5: - item.set_sensitive(spec[5]) - if len(spec) > 6 and spec[6] is not None: - item.set_submenu(createMenu(spec[6], radio, accel_group)) - item.set_use_underline(True) - else: - item = Gtk.SeparatorMenuItem.new() - item.show() - menu.append(item) - return menu - # masks used to indicate the presence of particular line endings DOS_FORMAT = 1 diff --git a/src/vcs/__init__.py b/src/diffuse/vcs/__init__.py similarity index 100% rename from src/vcs/__init__.py rename to src/diffuse/vcs/__init__.py diff --git a/src/vcs/bzr.py b/src/diffuse/vcs/bzr.py similarity index 100% rename from src/vcs/bzr.py rename to src/diffuse/vcs/bzr.py diff --git a/src/vcs/cvs.py b/src/diffuse/vcs/cvs.py similarity index 100% rename from src/vcs/cvs.py rename to src/diffuse/vcs/cvs.py diff --git a/src/vcs/darcs.py b/src/diffuse/vcs/darcs.py similarity index 100% rename from src/vcs/darcs.py rename to src/diffuse/vcs/darcs.py diff --git a/src/vcs/folder_set.py b/src/diffuse/vcs/folder_set.py similarity index 100% rename from src/vcs/folder_set.py rename to src/diffuse/vcs/folder_set.py diff --git a/src/vcs/git.py b/src/diffuse/vcs/git.py similarity index 100% rename from src/vcs/git.py rename to src/diffuse/vcs/git.py diff --git a/src/vcs/hg.py b/src/diffuse/vcs/hg.py similarity index 100% rename from src/vcs/hg.py rename to src/diffuse/vcs/hg.py diff --git a/src/vcs/mtn.py b/src/diffuse/vcs/mtn.py similarity index 100% rename from src/vcs/mtn.py rename to src/diffuse/vcs/mtn.py diff --git a/src/vcs/rcs.py b/src/diffuse/vcs/rcs.py similarity index 96% rename from src/vcs/rcs.py rename to src/diffuse/vcs/rcs.py index 710980d..8d8fe01 100644 --- a/src/vcs/rcs.py +++ b/src/diffuse/vcs/rcs.py @@ -60,7 +60,7 @@ class Rcs(VcsInterface): return result # simulate use of popen with xargs to read the output of a command - def _popen_xargs_readlines(self, dn, cmd, args, prefs, bash_pref): + def _popen_xargs_readlines(self, cmd, args, prefs, bash_pref): # os.sysconf() is only available on Unix if hasattr(os, 'sysconf'): maxsize = os.sysconf('SC_ARG_MAX') @@ -85,7 +85,7 @@ class Rcs(VcsInterface): s += len(args[i]) + 1 i += 1 if i == len(args) or not f: - ss.extend(utils.popenReadLines(dn, a, prefs, bash_pref)) + ss.extend(utils.popenReadLines(self.root, a, prefs, bash_pref)) s, a = 0, [] return ss @@ -138,7 +138,7 @@ class Rcs(VcsInterface): args = [ utils.safeRelativePath(self.root, k, prefs, 'rcs_cygwin') for k in r ] # run command r, k = {}, '' - for line in self._popen_xargs_readlines(self.root, cmd, args, prefs, 'rcs_bash'): + for line in self._popen_xargs_readlines(cmd, args, prefs, 'rcs_bash'): # parse response if line.startswith('Working file: '): k = prefs.convertToNativePath(line[14:]) diff --git a/src/vcs/svk.py b/src/diffuse/vcs/svk.py similarity index 90% rename from src/vcs/svk.py rename to src/diffuse/vcs/svk.py index 1e021c1..d4add43 100644 --- a/src/vcs/svk.py +++ b/src/diffuse/vcs/svk.py @@ -23,18 +23,22 @@ from diffuse import utils from diffuse.vcs.svn import Svn class Svk(Svn): - def _getVcs(self): + @staticmethod + def _getVcs(): return 'svk' - def _getURLPrefix(self): + @staticmethod + def _getURLPrefix(): return 'Depot Path: ' - def _parseStatusLine(self, s): + @staticmethod + def _parseStatusLine(s): if len(s) < 4 or s[0] not in 'ACDMR': return '', '' return s[0], s[4:] - def _getPreviousRevision(self, rev): + @staticmethod + def _getPreviousRevision(rev): if rev is None: return 'HEAD' if rev.endswith('@'): diff --git a/src/vcs/svn.py b/src/diffuse/vcs/svn.py similarity index 98% rename from src/vcs/svn.py rename to src/diffuse/vcs/svn.py index 339d0a5..7aded8b 100644 --- a/src/vcs/svn.py +++ b/src/diffuse/vcs/svn.py @@ -31,13 +31,16 @@ class Svn(VcsInterface): VcsInterface.__init__(self, root) self.url = None - def _getVcs(self): + @staticmethod + def _getVcs(): return 'svn' - def _getURLPrefix(self): + @staticmethod + def _getURLPrefix(): return 'URL: ' - def _parseStatusLine(self, s): + @staticmethod + def _parseStatusLine(s): if len(s) < 8 or s[0] not in 'ACDMR': return '', '' # subversion 1.6 adds a new column @@ -46,7 +49,8 @@ class Svn(VcsInterface): k += 1 return s[0], s[k:] - def _getPreviousRevision(self, rev): + @staticmethod + def _getPreviousRevision(rev): if rev is None: return 'BASE' m = int(rev) diff --git a/src/vcs/vcs_interface.py b/src/diffuse/vcs/vcs_interface.py similarity index 100% rename from src/vcs/vcs_interface.py rename to src/diffuse/vcs/vcs_interface.py diff --git a/src/vcs/vcs_registry.py b/src/diffuse/vcs/vcs_registry.py similarity index 100% rename from src/vcs/vcs_registry.py rename to src/diffuse/vcs/vcs_registry.py diff --git a/src/widgets.py b/src/diffuse/widgets.py similarity index 98% rename from src/widgets.py rename to src/diffuse/widgets.py index f6c625c..7a220d7 100644 --- a/src/widgets.py +++ b/src/diffuse/widgets.py @@ -1649,7 +1649,8 @@ class FileDiffViewer(Gtk.Grid): # scroll vertically to current line self._ensure_line_is_visible(current_line) - def __set_clipboard_text(self, clipboard, s): + @staticmethod + def _set_clipboard_text(clipboard, s): # remove embedded nulls as the clipboard cannot handle them Gtk.Clipboard.get(clipboard).set_text(s.replace('\0', ''), -1) @@ -1673,7 +1674,7 @@ class FileDiffViewer(Gtk.Grid): self.selection_char = sj if extend: - self.__set_clipboard_text(Gdk.SELECTION_PRIMARY, self.getSelectedText()) + self._set_clipboard_text(Gdk.SELECTION_PRIMARY, self.getSelectedText()) self._cursor_position_changed(True) self.emit('cursor_changed') @@ -1833,7 +1834,7 @@ class FileDiffViewer(Gtk.Grid): can_swap = (f != self.current_pane) # pylint: disable=line-too-long - menu = utils.createMenu( + menu = createMenu( [ [_('Align with Selection'), self.align_with_selection_cb, [f, i], Gtk.STOCK_EXECUTE, None, can_align], [_('Isolate'), self.button_cb, 'isolate', None, None, can_isolate ], [_('Merge Selection'), self.merge_lines_cb, f, None, None, can_merge], @@ -2922,7 +2923,7 @@ class FileDiffViewer(Gtk.Grid): # 'copy' action def copy(self): if self.mode in (LINE_MODE, CHAR_MODE): - self.__set_clipboard_text(Gdk.SELECTION_CLIPBOARD, self.getSelectedText()) + self._set_clipboard_text(Gdk.SELECTION_CLIPBOARD, self.getSelectedText()) # 'cut' action def cut(self): @@ -3685,6 +3686,50 @@ class FileDiffViewer(Gtk.Grid): def merge_from_right_then_left(self): self._mergeBoth(True) +# convenience method for creating a menu according to a template +def createMenu(specs, radio=None, accel_group=None): + menu = Gtk.Menu.new() + for spec in specs: + if len(spec) > 0: + if len(spec) > 7 and spec[7] is not None: + g, k = spec[7] + if g not in radio: + item = Gtk.RadioMenuItem.new_with_mnemonic_from_widget(None, spec[0]) + radio[g] = (item, {}) + else: + item = Gtk.RadioMenuItem.new_with_mnemonic_from_widget(radio[g][0], spec[0]) + radio[g][1][k] = item + else: + item = Gtk.ImageMenuItem.new_with_mnemonic(spec[0]) + cb = spec[1] + if cb is not None: + data = spec[2] + item.connect('activate', cb, data) + if len(spec) > 3 and spec[3] is not None: + image = Gtk.Image.new() + image.set_from_stock(spec[3], Gtk.IconSize.MENU) # pylint: disable=no-member + item.set_image(image) + if accel_group is not None and len(spec) > 4: + a = theResources.getKeyBindings('menu', spec[4]) + if len(a) > 0: + key, modifier = a[0] + item.add_accelerator( + 'activate', + accel_group, + key, + modifier, + Gtk.AccelFlags.VISIBLE) + if len(spec) > 5: + item.set_sensitive(spec[5]) + if len(spec) > 6 and spec[6] is not None: + item.set_submenu(createMenu(spec[6], radio, accel_group)) + item.set_use_underline(True) + else: + item = Gtk.SeparatorMenuItem.new() + item.show() + menu.append(item) + return menu + ALPHANUMERIC_CLASS = 0 WHITESPACE_CLASS = 1 OTHER_CLASS = 2 diff --git a/src/meson.build b/src/meson.build index eccac61..1e252f9 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,44 +1 @@ -pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name()) -moduledir = join_paths(pkgdatadir, meson.project_name()) -sysconfdir = join_paths(get_option('prefix'), get_option('sysconfdir')) - -python = import('python') - -conf = configuration_data() -conf.set('PYTHON', python.find_installation('python3').path()) -conf.set('VERSION', meson.project_version()) -conf.set('localedir', join_paths(get_option('prefix'), get_option('localedir'))) -conf.set('pkgdatadir', pkgdatadir) -conf.set('sysconfigdir', sysconfdir) -conf.set('log_print_output', get_option('log_print_output')) -conf.set('log_print_stack', get_option('log_print_stack')) -conf.set('use_flatpak', get_option('use_flatpak')) - -configure_file( - input: 'diffuse.in', - output: 'diffuse', - configuration: conf, - install: true, - install_dir: get_option('bindir') -) - -configure_file( - input: 'constants.py.in', - output: 'constants.py', - configuration: conf, - install: true, - install_dir: moduledir -) - -diffuse_sources = [ - '__init__.py', - 'dialogs.py', - 'main.py', - 'preferences.py', - 'resources.py', - 'utils.py', - 'widgets.py', -] - -install_data(diffuse_sources, install_dir: moduledir) -install_subdir('vcs', install_dir: moduledir, strip_directory: false) +subdir('diffuse') diff --git a/windows-installer/build.py b/windows-installer/build.py index b137bb0..490c6e6 100755 --- a/windows-installer/build.py +++ b/windows-installer/build.py @@ -37,7 +37,7 @@ def mkdir(s): # copies a file to 'dest' def copyFile(src, dest, use_text_mode=False,enc=None): - print 'copying "%s" to "%s"' % (src, dest) + print('copying "%s" to "%s"' % (src, dest)) if use_text_mode: r, w = 'r', 'w' else: @@ -53,7 +53,7 @@ def copyFile(src, dest, use_text_mode=False,enc=None): # recursively copies a directory to 'dest' def copyDir(src, dest): - print 'copying "%s" to "%s"' % (src, dest) + print('copying "%s" to "%s"' % (src, dest)) mkdir(dest) for f in os.listdir(src): s = os.path.join(src, f) @@ -128,7 +128,7 @@ locale_dir = os.path.join(gtk_dir, 'share\\locale') for s in glob.glob('..\\po\\*.po'): lang = s[16:-3] # Diffuse localisations - print 'Compiling %s translation' % (lang, ) + print('Compiling %s translation' % (lang, )) d = 'dist' for p in [ 'locale', lang, 'LC_MESSAGES' ]: d = os.path.join(d, p) @@ -236,4 +236,4 @@ if os.system('iscc diffuse.iss /F%s' % (INSTALLER, )) != 0: # Declare success. # -print 'Successfully created "%s".' % (INSTALLER, ) +print('Successfully created "%s".' % (INSTALLER, ))