From 70b1bdbb9f10f21f593097cf58b7f3d44cb14729 Mon Sep 17 00:00:00 2001 From: Romain Failliot Date: Sat, 27 Nov 2021 21:16:35 -0500 Subject: [PATCH] No python files are processed by Meson anymore * Linters can be run sooner (before install) * Moved some constants in the config file * Added a new keyword in the config syntax: "option" * Better messages when an error occurs while parsing the config --- data/diffuserc.in | 8 +- data/meson.build | 3 + src/diffuse/{constants.py.in => constants.py} | 12 +- src/diffuse/dialogs.py | 2 + src/diffuse/diffuse.in | 13 +- src/diffuse/main.py | 23 ++-- src/diffuse/meson.build | 18 +-- src/diffuse/preferences.py | 2 + src/diffuse/resources.py | 119 +++++++++++++----- src/diffuse/utils.py | 9 +- src/diffuse/vcs/cvs.py | 2 + src/diffuse/vcs/folder_set.py | 6 +- src/diffuse/vcs/rcs.py | 2 + src/diffuse/vcs/svn.py | 2 + src/diffuse/vcs/vcs_registry.py | 2 + src/diffuse/widgets.py | 3 +- 16 files changed, 151 insertions(+), 75 deletions(-) rename src/diffuse/{constants.py.in => constants.py} (85%) diff --git a/data/diffuserc.in b/data/diffuserc.in index 9a2f221..3996f7d 100644 --- a/data/diffuserc.in +++ b/data/diffuserc.in @@ -1,5 +1,7 @@ -# /etc/diffuserc: System-wide initialisation file for Diffuse -# -# Copyright (C) 2006-2009 Derrick Moser +# System-wide initialization file for Diffuse + +option log_print_output @LOG_PRINT_OUTPUT@ +option log_print_stack @LOG_PRINT_STACK@ +option use_flatpak @USE_FLATPAK@ import @PKGDATADIR@/syntax/*.syntax diff --git a/data/meson.build b/data/meson.build index f3a6234..f704ec1 100644 --- a/data/meson.build +++ b/data/meson.build @@ -33,6 +33,9 @@ endif # Diffuse config file conf = configuration_data() +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')) conf.set('PKGDATADIR', pkgdatadir) configure_file( diff --git a/src/diffuse/constants.py.in b/src/diffuse/constants.py similarity index 85% rename from src/diffuse/constants.py.in rename to src/diffuse/constants.py index 241394c..cb01b40 100644 --- a/src/diffuse/constants.py.in +++ b/src/diffuse/constants.py @@ -17,14 +17,12 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +from gettext import gettext as _ + APP_NAME = 'Diffuse' -VERSION = '@VERSION@' COPYRIGHT = '''{copyright} © 2006-2019 Derrick Moser -{copyright} © 2015-2021 Romain Failliot'''.format(copyright=_("Copyright")) # type: ignore +{copyright} © 2015-2021 Romain Failliot'''.format(copyright=_("Copyright")) WEBSITE = 'https://mightycreak.github.io/diffuse/' -SYSCONFIGDIR = '@SYSCONFIGDIR@' - -LOG_PRINT_OUTPUT = @LOG_PRINT_OUTPUT@ -LOG_PRINT_STACK = @LOG_PRINT_STACK@ -USE_FLATPAK = @USE_FLATPAK@ +# Constants are set in main() +VERSION = '0.0.0' diff --git a/src/diffuse/dialogs.py b/src/diffuse/dialogs.py index edd3388..7b8a030 100644 --- a/src/diffuse/dialogs.py +++ b/src/diffuse/dialogs.py @@ -19,6 +19,8 @@ import os +from gettext import gettext as _ + from diffuse import constants from diffuse import utils diff --git a/src/diffuse/diffuse.in b/src/diffuse/diffuse.in index 738a48c..4a60c6b 100755 --- a/src/diffuse/diffuse.in +++ b/src/diffuse/diffuse.in @@ -22,9 +22,16 @@ import sys import gettext -sys.path.insert(1, '@PKGDATADIR@') -gettext.install('diffuse', '@LOCALEDIR@') +VERSION = '@VERSION@' +PKGDATADIR = '@PKGDATADIR@' +LOCALEDIR = '@LOCALEDIR@' +SYSCONFIGDIR = '@SYSCONFIGDIR@' + +sys.path.insert(1, PKGDATADIR) + +gettext.bindtextdomain('diffuse', localedir=LOCALEDIR) +gettext.textdomain('diffuse') if __name__ == '__main__': from diffuse import main - sys.exit(main.main()) + sys.exit(main.main(VERSION, SYSCONFIGDIR)) diff --git a/src/diffuse/main.py b/src/diffuse/main.py index e26a3c8..54e7af1 100644 --- a/src/diffuse/main.py +++ b/src/diffuse/main.py @@ -25,6 +25,7 @@ import shlex import stat import webbrowser +from gettext import gettext as _ from urllib.parse import urlparse from diffuse import constants @@ -958,7 +959,7 @@ class Diffuse(Gtk.Window): self.int_state['window_width'] = event.width self.int_state['window_height'] = event.height - # record the window's maximised state + # record the window's maximized state def window_state_cb(self, window, event): self.bool_state['window_maximized'] = ( (event.new_window_state & Gdk.WindowState.MAXIMIZED) != 0 @@ -1309,7 +1310,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 @@ -1614,7 +1615,7 @@ class Diffuse(Gtk.Window): help_url = None if utils.isWindows(): # help documentation is distributed as local HTML files - # search for localised manual first + # search for localized manual first parts = ['manual'] if utils.lang is not None: parts = ['manual'] @@ -1637,7 +1638,7 @@ class Diffuse(Gtk.Window): browser = fp break if browser is not None: - # find localised help file + # find localized help file if utils.lang is None: parts = [] else: @@ -1661,7 +1662,7 @@ class Diffuse(Gtk.Window): if help_url is None: # no local help file is available, show on-line help help_url = constants.WEBSITE + 'manual.html' - # ask for localised manual + # ask for localized manual if utils.lang is not None: help_url += '?lang=' + utils.lang # use a web browser to display the help documentation @@ -1752,13 +1753,15 @@ GObject.signal_new('save', Diffuse.FileDiffViewer.PaneHeader, GObject.SignalFlag GObject.signal_new('save-as', Diffuse.FileDiffViewer.PaneHeader, GObject.SignalFlags.RUN_LAST, GObject.TYPE_NONE, ()) # noqa: E501 -def main(): +def main(version, sysconfigdir): # app = Application() # return app.run(sys.argv) args = sys.argv argc = len(args) + constants.VERSION = version + if argc == 2 and args[1] in ['-v', '--version']: print('%s %s\n%s' % (constants.APP_NAME, constants.VERSION, constants.COPYRIGHT)) return 0 @@ -1828,25 +1831,23 @@ Display Options: rc_files.append(args[i]) i += 1 else: - # parse system wide then personal initialisation files + # parse system wide then personal initialization files if utils.isWindows(): rc_file = os.path.join(utils.bin_dir, 'diffuserc') else: - rc_file = os.path.join(utils.bin_dir, f'{constants.SYSCONFIGDIR}/diffuserc') + rc_file = os.path.join(utils.bin_dir, f'{sysconfigdir}/diffuserc') for rc_file in rc_file, os.path.join(rc_dir, 'diffuserc'): if os.path.isfile(rc_file): rc_files.append(rc_file) for rc_file in rc_files: # convert to absolute path so the location of any processing errors are - # reported with normalised file names + # reported with normalized file names rc_file = os.path.abspath(rc_file) try: - # diffuse.theResources.parse(rc_file) # Modularization theResources.parse(rc_file) except IOError: utils.logError(_('Error reading %s.') % (rc_file, )) - # diff = diffuse.Diffuse(rc_dir) # Modularization diff = Diffuse(rc_dir) # load state diff --git a/src/diffuse/meson.build b/src/diffuse/meson.build index 22c2270..6b27994 100644 --- a/src/diffuse/meson.build +++ b/src/diffuse/meson.build @@ -5,8 +5,10 @@ python = import('python') conf = configuration_data() conf.set('PYTHON', python.find_installation('python3').path()) +conf.set('VERSION', meson.project_version()) conf.set('PKGDATADIR', pkgdatadir) conf.set('LOCALEDIR', join_paths(get_option('prefix'), get_option('localedir'))) +conf.set('SYSCONFIGDIR', join_paths(get_option('prefix'), get_option('sysconfdir'))) configure_file( input: 'diffuse.in', @@ -16,23 +18,9 @@ configure_file( install_dir: get_option('bindir') ) -conf = configuration_data() -conf.set('VERSION', meson.project_version()) -conf.set('SYSCONFIGDIR', join_paths(get_option('prefix'), get_option('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', + 'constants.py', 'dialogs.py', 'main.py', 'preferences.py', diff --git a/src/diffuse/preferences.py b/src/diffuse/preferences.py index 1a0b215..7d92766 100644 --- a/src/diffuse/preferences.py +++ b/src/diffuse/preferences.py @@ -23,6 +23,8 @@ import os import shlex import sys +from gettext import gettext as _ + from diffuse import constants from diffuse import utils diff --git a/src/diffuse/resources.py b/src/diffuse/resources.py index d2c19f8..8470f2e 100644 --- a/src/diffuse/resources.py +++ b/src/diffuse/resources.py @@ -17,18 +17,21 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -# This class to hold all customisable behaviour not exposed in the preferences +# This class to hold all customizable behavior not exposed in the preferences # dialogue: hotkey assignment, colours, syntax highlighting, etc. # Syntax highlighting is implemented in supporting '*.syntax' files normally -# read from the system wide initialisation file '/etc/diffuserc'. -# The personal initialisation file '~/diffuse/diffuserc' can be used to change -# default behaviour. +# read from the system wide initialization file '/etc/diffuserc'. +# The personal initialization file '~/diffuse/diffuserc' can be used to change +# default behavior. import glob import os import re import shlex +from distutils import util +from gettext import gettext as _ + from diffuse import utils import gi # type: ignore @@ -197,6 +200,13 @@ class Resources: 'line_selection_opacity': 0.4 } + # default options + self.options = { + 'log_print_output': 'False', + 'log_print_stack': 'False', + 'use_flatpak': 'False' + } + # default strings self.strings = {} @@ -225,14 +235,14 @@ class Resources: elif token == 'Alt': modifiers |= Gdk.ModifierType.MOD1_MASK elif len(token) == 0 or token[0] == '_': - raise ValueError() + raise ValueError(_('The key binding "{key}" is invalid').format(key=v)) else: token = 'KEY_' + token if not hasattr(Gdk, token): - raise ValueError() + raise ValueError(_('The key binding "{key}" is invalid').format(key=v)) key = getattr(Gdk, token) if key is None: - raise ValueError() + raise ValueError(_('The key binding "{key}" is invalid').format(key=v)) key_tuple = (ctx, (key, modifiers)) # remove any existing binding @@ -298,14 +308,25 @@ class Resources: self.floats[symbol] = v = 0.5 return v + def getOption(self, option: str) -> str: + '''Get the option value.''' + try: + return self.options[option] + except KeyError: + utils.logDebug(f'Warning: unknown option "{option}"') + return '' + + def getOptionAsBool(self, option: str) -> bool: + '''Get the option value, casted as a boolean.''' + return util.strtobool(self.getOption(option)) + # string resources def getString(self, symbol): try: return self.strings[symbol] except KeyError: utils.logDebug(f'Warning: unknown string "{symbol}"') - self.strings[symbol] = v = '' - return v + return '' # syntax highlighting def getSyntaxNames(self): @@ -346,7 +367,9 @@ class Resources: try: # eg. add Python syntax highlighting: # import /usr/share/diffuse/syntax/python.syntax - if args[0] == 'import' and len(args) == 2: + if args[0] == 'import': + if len(args) != 2: + raise SyntaxError(_('Imports must have one argument')) path = os.path.expanduser(args[1]) # relative paths are relative to the parsed file path = os.path.join(utils.globEscape(os.path.dirname(file_name)), path) @@ -356,23 +379,41 @@ class Resources: for path in paths: # convert to absolute path so the location of # any processing errors are reported with - # normalised file names + # normalized file names self.parse(os.path.abspath(path)) # eg. make Ctrl+o trigger the open_file menu item # keybinding menu open_file Ctrl+o - elif args[0] == 'keybinding' and len(args) == 4: + elif args[0] == 'keybinding': + if len(args) != 4: + raise SyntaxError(_('Key bindings must have three arguments')) self.setKeyBinding(args[1], args[2], args[3]) # eg. set the regular background colour to white # colour text_background 1.0 1.0 1.0 - elif args[0] in ['colour', 'color'] and len(args) == 5: + elif args[0] in ['colour', 'color']: + if len(args) != 5: + raise SyntaxError(_('Colors must have four arguments')) self.colours[args[1]] = _Colour(float(args[2]), float(args[3]), float(args[4])) # eg. set opacity of the line_selection colour # float line_selection_opacity 0.4 - elif args[0] == 'float' and len(args) == 3: + elif args[0] == 'float': + if len(args) != 3: + raise SyntaxError(_('Floats must have two arguments')) self.floats[args[1]] = float(args[2]) + # eg. enable option log_print_output + # option log_print_output true + elif args[0] == 'option': + if len(args) != 3: + raise SyntaxError(_('Options must have two arguments')) + if args[1] not in self.options: + raise SyntaxError( + _('Options "{option}" is unknown').format(option=args[1]) + ) + self.options[args[1]] = args[2] # eg. set the help browser # string help_browser gnome-help - elif args[0] == 'string' and len(args) == 3: + elif args[0] == 'string': + if len(args) != 3: + raise SyntaxError(_('Strings must have two arguments')) self.strings[args[1]] = args[2] if args[1] == 'difference_colours': self.setDifferenceColours(args[2]) @@ -381,7 +422,9 @@ class Resources: # where 'normal' is the name of the default state and # 'text' is the classification of all characters not # explicitly matched by a syntax highlighting rule - elif args[0] == 'syntax' and (len(args) == 2 or len(args) == 4): + elif args[0] == 'syntax': + if len(args) != 3 and len(args) != 4: + raise SyntaxError(_('Syntaxes must have two or three arguments')) key = args[1] if len(args) == 2: # remove file pattern for a syntax specification @@ -407,17 +450,15 @@ class Resources: # the pattern '#' is matched and classify the matched # characters as 'python_comment' # syntax_pattern normal comment python_comment '#' - elif ( - args[0] == 'syntax_pattern' and - self.current_syntax is not None and - len(args) >= 5 - ): + elif args[0] == 'syntax_pattern' and self.current_syntax is not None: + if len(args) < 5: + raise SyntaxError(_('Syntax patterns must have at least four arguments')) flags = 0 for arg in args[5:]: if arg == 'ignorecase': flags |= re.IGNORECASE else: - raise ValueError() + raise SyntaxError(_('Value "{value}" is unknown').format(value=arg)) self.current_syntax.addPattern( args[1], args[2], @@ -426,7 +467,9 @@ class Resources: # eg. default to the Python syntax rules when viewing # a file ending with '.py' or '.pyw' # syntax_files Python '\.pyw?$' - elif args[0] == 'syntax_files' and (len(args) == 2 or len(args) == 3): + elif args[0] == 'syntax_files': + if len(args) != 2 and len(args) != 3: + raise SyntaxError(_('Syntax files must have one or two arguments')) key = args[1] if len(args) == 2: # remove file pattern for a syntax specification @@ -442,7 +485,9 @@ class Resources: # eg. default to the Python syntax rules when viewing # a files starting with patterns like #!/usr/bin/python # syntax_magic Python '^#!/usr/bin/python$' - elif args[0] == 'syntax_magic' and len(args) > 1: + elif args[0] == 'syntax_magic': + if len(args) < 2: + raise SyntaxError(_('Syntax magics must have at least one argument')) key = args[1] if len(args) == 2: # remove magic pattern for a syntax specification @@ -456,16 +501,30 @@ class Resources: if arg == 'ignorecase': flags |= re.IGNORECASE else: - raise ValueError() + raise SyntaxError( + _('Value "{value}" is unknown').format(value=arg) + ) self.syntax_magic_patterns[key] = re.compile(args[2], flags) else: - raise ValueError() - # except ValueError: - except: # noqa: E722 # Grr... the 're' module throws weird errors - utils.logError(_('Error processing line {line} of {file}.'.format( + raise SyntaxError(_('Keyword "{keyword}" is unknown').format(keyword=args[0])) + except SyntaxError as e: + error_msg = _('Syntax error at line {line} of {file}').format( line=i + 1, file=file_name - ))) + ) + utils.logError(f'{error_msg}: {e.msg}') + except ValueError as e: + error_msg = _('Value error at line {line} of {file}').format( + line=i + 1, + file=file_name + ) + utils.logError(f'{error_msg}: {e.msg}') + except re.error: + error_msg = _('Regex error at line {line} of {file}.') + utils.logError(error_msg.format(line=i + 1, file=file_name)) + except: # noqa: E722 + error_msg = _('Unhandled error at line {line} of {file}.') + utils.logError(error_msg.format(line=i + 1, file=file_name)) # colour resources diff --git a/src/diffuse/utils.py b/src/diffuse/utils.py index 65a52b7..77ba3c3 100644 --- a/src/diffuse/utils.py +++ b/src/diffuse/utils.py @@ -23,7 +23,10 @@ import locale import subprocess import traceback +from gettext import gettext as _ + from diffuse import constants +from diffuse.resources import theResources import gi # type: ignore gi.require_version('Gtk', '3.0') @@ -77,9 +80,9 @@ def isWindows(): def _logPrintOutput(msg): - if constants.LOG_PRINT_OUTPUT: + if theResources.getOptionAsBool('log_print_output'): print(msg, file=sys.stderr) - if constants.LOG_PRINT_STACK: + if theResources.getOptionAsBool('log_print_stack'): traceback.print_stack() @@ -154,7 +157,7 @@ def _bash_escape(s): def _use_flatpak(): - return constants.USE_FLATPAK + return theResources.getOptionAsBool('use_flatpak') # use popen to read the output of a command diff --git a/src/diffuse/vcs/cvs.py b/src/diffuse/vcs/cvs.py index 56f33a2..306f2e2 100644 --- a/src/diffuse/vcs/cvs.py +++ b/src/diffuse/vcs/cvs.py @@ -19,6 +19,8 @@ import os +from gettext import gettext as _ + from diffuse import utils from diffuse.vcs.folder_set import FolderSet from diffuse.vcs.vcs_interface import VcsInterface diff --git a/src/diffuse/vcs/folder_set.py b/src/diffuse/vcs/folder_set.py index da1e0ba..df36555 100644 --- a/src/diffuse/vcs/folder_set.py +++ b/src/diffuse/vcs/folder_set.py @@ -22,8 +22,10 @@ import os class FolderSet: '''Utility class to help support Git and Monotone. - Represents a set of files and folders of interest for "git status" or - "mtn automate inventory."''' + + Represents a set of files and folders of interest for "git status" or + "mtn automate inventory." + ''' def __init__(self, names): self.folders = f = [] diff --git a/src/diffuse/vcs/rcs.py b/src/diffuse/vcs/rcs.py index b6c0b84..41fa203 100644 --- a/src/diffuse/vcs/rcs.py +++ b/src/diffuse/vcs/rcs.py @@ -19,6 +19,8 @@ import os +from gettext import gettext as _ + from diffuse import utils from diffuse.vcs.vcs_interface import VcsInterface diff --git a/src/diffuse/vcs/svn.py b/src/diffuse/vcs/svn.py index 95824c5..2db5390 100644 --- a/src/diffuse/vcs/svn.py +++ b/src/diffuse/vcs/svn.py @@ -20,6 +20,8 @@ import os import glob +from gettext import gettext as _ + from diffuse import utils from diffuse.vcs.folder_set import FolderSet from diffuse.vcs.vcs_interface import VcsInterface diff --git a/src/diffuse/vcs/vcs_registry.py b/src/diffuse/vcs/vcs_registry.py index 5517339..6e330c2 100644 --- a/src/diffuse/vcs/vcs_registry.py +++ b/src/diffuse/vcs/vcs_registry.py @@ -19,6 +19,8 @@ import os +from gettext import gettext as _ + from diffuse import utils from diffuse.vcs.folder_set import FolderSet from diffuse.vcs.bzr import Bzr diff --git a/src/diffuse/widgets.py b/src/diffuse/widgets.py index 5adb41e..95c4a77 100644 --- a/src/diffuse/widgets.py +++ b/src/diffuse/widgets.py @@ -21,6 +21,7 @@ import difflib import os import unicodedata +from gettext import gettext as _ from typing import Dict from diffuse import utils @@ -641,7 +642,7 @@ class FileDiffViewerBase(Gtk.Grid): panes = self.panes else: panes = [self.panes[f]] - for _, pane in enumerate(panes): + for pane in panes: del pane.syntax_cache[:] del pane.diff_cache[:] # re-compute the high water mark