From e818aeb2b009e2be052685c70dae202c88a00aee Mon Sep 17 00:00:00 2001 From: buhtz Date: Fri, 15 Mar 2024 09:22:16 +0100 Subject: [PATCH] fix: Debug output with version, name, run-as-root and OS The debug output now contains minimal diagnostic information about appliation name, version, if it runs as root and the operating system. - Separated code from `collect_diagnostics()` into `collect_minimal_diagnostics()` which is then called in `common/backintime.py::startApp()` to build a debug message. - Fixed an the user-callback unit tests introduced in #1658 - Minor refactoring and minor mods in README.md and CONTRIBUTING.md. Fix #1664 Improve PR #1658 --- .travis.yml | 4 +- CONTRIBUTING.md | 6 +-- README.md | 41 ++++++++------- common/backintime.py | 68 ++++++++++++++++--------- common/config.py | 1 + common/diagnostics.py | 59 ++++++++++++--------- common/plugins/usercallbackplugin.py | 2 +- common/qt_probing.py | 7 ++- common/test/test_diagnostics.py | 29 ++++++++--- common/test/test_lint.py | 26 ++++++++-- common/test/test_plugin_usercallback.py | 9 +++- common/tools.py | 6 +-- qt/aboutdlg.py | 2 +- qt/languagedialog.py | 3 +- qt/logviewdialog.py | 1 - qt/serviceHelper.py | 12 +++-- qt/test/test_lint.py | 26 ++++++++-- 17 files changed, 198 insertions(+), 104 deletions(-) diff --git a/.travis.yml b/.travis.yml index cf0516fea..a92b3946c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -57,9 +57,9 @@ jobs: python: "3.12" install: - - pip install pylint coveralls pyfakefs + - pip install pylint coveralls pyfakefs keyring # PyQt is not available for "ppc64le" architecture on PyPi - - if [ "$TRAVIS_ARCH" != "ppc64le" ] ; then pip install pyqt6; fi + - if [ "$TRAVIS_ARCH" != "ppc64le" ] ; then pip install pyqt6 dbus-python; fi # add ssh public / private key pair to ensure user can start ssh session to localhost for tests - ssh-keygen -b 2048 -t rsa -f /home/travis/.ssh/id_rsa -N "" - cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dd29ded6b..c85f27e95 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -90,8 +90,8 @@ replaced with PyPi packages. - `python3-dbus.mainloop.pyqt6` - `libnotify-bin` - `policykit-1` - - `qttranslations5-l10n` - - `qtwayland5` (if Wayland is used as display server instead of X11) + - `qttranslations6-l10n` + - `qtwayland6` (if Wayland is used as display server instead of X11) - Recommended - For SSH key storage **one** of these packages - `python3-secretstorage` @@ -151,4 +151,4 @@ Keep in mind as you contribute, that code, docs and other material submitted to the project are considered licensed under the same terms (see [LICENSE](LICENSE)) as the rest of the work. -Sept 2023 +March 2024 diff --git a/README.md b/README.md index d9d1885f3..f2182d6a1 100644 --- a/README.md +++ b/README.md @@ -7,21 +7,15 @@ Copyright (C) 2008-2024 Oprea Dan, Bart de Koning, Richard Bailey, Germar Reitze, Taylor Raack, Christian Buhtz, Michael Büker, Jürgen Altfeld -_Back In Time_ is an easy-to-use backup tool for files and folders. +_Back In Time_ is an easy-to-use tool to backup files and folders. It runs on GNU Linux (not on Windows or OS X/macOS) and provides a command line tool `backintime` and a GUI `backintime-qt` both written in Python3. It uses [`rsync`](https://rsync.samba.org/) to take manual or scheduled snapshots and stores them locally or remotely through SSH. Each snapshot is in its own folder with copies of the original files, but unchanged files are hard-linked between -snapshots to save space. +snapshots to save storage space. It was inspired by [FlyBack](https://en.wikipedia.org/wiki/FlyBack). -You only need to specify 3 things: - -* What folders to back up. -* Where to save snapshots. -* The backup frequency (manual, every hour, every day, every month). - ## Maintenance status A small team (Christian Buhtz, Michael Büker and Jürgen Altfeld) @@ -37,7 +31,7 @@ instead of implementing new If you are interested in the development, please see [CONTRIBUTING](CONTRIBUTING.md) and have a look on [open issues](https://github.com/bit-team/backintime/issues) especially -those labeled as [good first](https://github.com/bit-team/backintime/labels/GOOD%20FIRST%20ISSUE) +those labeled as [good first issue](https://github.com/bit-team/backintime/labels/GOOD%20FIRST%20ISSUE) and [help wanted](https://github.com/bit-team/backintime/issues?q=is%3Aissue+is%3Aopen+label%3AHELP-WANTED). ## Index @@ -49,14 +43,15 @@ and [help wanted](https://github.com/bit-team/backintime/issues?q=is%3Aissue+is% ## Documentation, FAQs, Support - * [End user documentation](https://backintime.readthedocs.org/) (not totally up-to-date) * [FAQ - Frequently Asked Questions](FAQ.md) - * [Source code documentation for developers](https://backintime-dev.readthedocs.org) - * Use [Issues](https://github.com/bit-team/backintime/issues) to ask questions and report bugs. + * [End user documentation](https://backintime.readthedocs.org/) (not totally up-to-date) * [Mailing list _bit-dev_](https://mail.python.org/mailman3/lists/bit-dev.python.org/) for **every topic**, question and idea about _Back In Time_. Despite its name it is not restricted to development topics only. + * Use [Issues](https://github.com/bit-team/backintime/issues) to ask + questions and report bugs. + * [Source code documentation for developers](https://backintime-dev.readthedocs.org) ## Installation @@ -77,11 +72,11 @@ installation options provided and maintained by third parties. In the latest stable release: - [File permissions handling and therefore possible non-differential backups](#file-permissions-handling-and-therefore-possible-non-differential-backups) -- RTE "module 'qttools' has no attribute 'initate_translator'" with encFS when prompting the user for a password (#1553) - [Warning: apt-key is deprecated. Manage keyring files in trusted.gpg.d instead (see apt-key(8)).](#warning-apt-key-is-deprecated-manage-keyring-files-in-trustedgpgd-instead-see-apt-key8) - [`qt5_probing.py` may hang with high CPU usage when running BiT as `root` via `cron`](#qt5_probingpy-may-hang-with-high-cpu-usage-when-running-bit-as-root-via-cron) In older releases: +- RTE "module 'qttools' has no attribute 'initate_translator'" with encFS when prompting the user for a password ([#1553](https://github.com/bit-team/backintime/issues/#1553)) - [Tray icon or other icons not shown correctly](#tray-icon-or-other-icons-not-shown-correctly) - [Non-working password safe and BiT forgets passwords (keyring backend issues)](#non-working-password-safe-and-bit-forgets-passwords-keyring-backend-issues) - [Incompatibility with rsync >= 3.2.4](#incompatibility-with-rsync-324-or-newer) @@ -120,7 +115,7 @@ This issue is tracked in [#1338](https://github.com/bit-team/backintime/issues/1 #### `qt5_probing.py` may hang with high CPU usage when running BiT as `root` via `cron` -See the related issue #1592 +See the related issue [#1592](https://github.com/bit-team/backintime/issues/1592). The only reliable work-around is to delete (or move into another folder) the file `/usr/share/backintime/common/qt5_probing.py`: @@ -184,15 +179,25 @@ See also issue [#1321](https://github.com/bit-team/backintime/issues/1321) #### Incompatibility with rsync 3.2.4 or newer -The release (`1.3.2`) and earlier versions of _Back In Time_ are incompatible with `rsync >= 3.2.4` ([#1247](https://github.com/bit-team/backintime/issues/1247)). The problem is [fixed](https://github.com/bit-team/backintime/pull/1351) in the current master branch of that repo and will be released with the next release (`1.3.3`) of _Back In Time_. +**Status: Fixed in v1.3.3** + +The release (`1.3.2`) and earlier versions of _Back In Time_ are incompatible +with `rsync >= 3.2.4` +([#1247](https://github.com/bit-team/backintime/issues/1247)). -If you use `rsync >= 3.2.4` and `backintime <= 1.3.2` there is a workaround. Add `--old-args` in [_Expert Options_ / _Additional options to rsync_](https://backintime.readthedocs.io/en/latest/settings.html#expert-options). Note that some GNU/Linux distributions (e.g. Manjaro) using a workaround with environment variable `RSYNC_OLD_ARGS` in their distro-specific packages for _Back In Time_. In that case you may not see any problems. +If you use `rsync >= 3.2.4` and `backintime <= 1.3.2` there is a +workaround. Add `--old-args` in +[_Expert Options_ / _Additional options to rsync_](https://backintime.readthedocs.io/en/latest/settings.html#expert-options). +Note that some GNU/Linux distributions (e.g. Manjaro) using a workaround with +environment variable `RSYNC_OLD_ARGS` in their distro-specific packages for +_Back In Time_. In that case you may not see any problems. #### Python 3.10 compatibility and Ubuntu version _Back In Time_ versions older than 1.3.2 do not start with Python >= 3.10. Ubuntu 22.04 LTS ships with Python 3.10 and backintime 1.2.1, but has applied [a patch](https://bugs.launchpad.net/ubuntu/+source/backintime/+bug/1976164/+attachment/5593556/+files/backintime_1.2.1-3_1.2.1-3ubuntu0.1.diff) -to make it work. If you want to update to backintime 1.3.2 in Ubuntu, you may use the PPA: see under [`INSTALL/Ubuntu PPA`](#Ubuntu-PPA). +to make it work. If you want to update _Back In Time_, you may use one of the +[alternative options for installation](#alternative-installation-options). -Jan 2024 +March 2024 diff --git a/common/backintime.py b/common/backintime.py index b2ca974de..9ff047f55 100644 --- a/common/backintime.py +++ b/common/backintime.py @@ -37,7 +37,7 @@ import password import encfstools import cli -from diagnostics import collect_diagnostics +from diagnostics import collect_diagnostics, collect_minimal_diagnostics from exceptions import MountException from applicationinstance import ApplicationInstance from version import __version__ @@ -504,14 +504,18 @@ def startApp(app_name = 'backintime'): logger.openlog() - #parse args args = argParse(None) - #add source path to $PATH environ if running from source + # Name, Version, As Root, OS + diag = collect_minimal_diagnostics() + logger.debug( + f'{diag["backintime"]} {list(diag["host-setup"]["OS"].values())}') + + # Add source path to $PATH environ if running from source if tools.runningFromSource(): tools.addSourceToPathEnviron() - #warn about sudo + # Warn about sudo if tools.usingSudo() and os.getenv('BIT_SUDO_WARNING_PRINTED', 'false') == 'false': os.putenv('BIT_SUDO_WARNING_PRINTED', 'true') logger.warning("It looks like you're using 'sudo' to start %(app)s. " @@ -519,9 +523,10 @@ def startApp(app_name = 'backintime'): "or 'pkexec %(app_name)s'." %{'app_name': app_name, 'app': config.Config.APP_NAME}) - #call commands + # Call commands if 'func' in dir(args): args.func(args) + else: setQuiet(args) printHeader() @@ -550,51 +555,64 @@ def join(args, subArgs): that should be merged into ``args`` """ for key, value in vars(subArgs).items(): - #only add new values if it isn't set already or if there really IS a value + # Only add new values if it isn't set already or if there really IS + # a value if getattr(args, key, None) is None or value: setattr(args, key, value) - #first parse the main parser without subparsers - #otherwise positional args in subparsers will be to greedy - #but only if -h or --help is not involved because otherwise - #help will not work for subcommands + # First parse the main parser without subparsers + # otherwise positional args in subparsers will be to greedy + # but only if -h or --help is not involved because otherwise + # help will not work for subcommands mainParser = parsers['main'] sub = [] + if '-h' not in sys.argv and '--help' not in sys.argv: + for i in mainParser._actions: + if isinstance(i, argparse._SubParsersAction): - #remove subparsers + # Remove subparsers mainParser._remove_action(i) sub.append(i) + args, unknownArgs = mainParser.parse_known_args(args) - #read subparsers again + + # Read subparsers again if sub: [mainParser._add_action(i) for i in sub] - #parse it again for unknown args + # Parse it again for unknown args if unknownArgs: subArgs, unknownArgs = mainParser.parse_known_args(unknownArgs) join(args, subArgs) - #finally parse only the command parser, otherwise we miss - #some arguments from command + # Finally parse only the command parser, otherwise we miss some arguments + # from command if unknownArgs and 'command' in args and args.command in parsers: commandParser = parsers[args.command] subArgs, unknownArgs = commandParser.parse_known_args(unknownArgs) join(args, subArgs) - if 'debug' in args: + try: logger.DEBUG = args.debug - - dargs = vars(args) - logger.debug('Arguments: %s | unknownArgs: %s' - %({arg:dargs[arg] for arg in dargs if dargs[arg]}, - unknownArgs)) - - #report unknown arguments - #but not if we run aliasParser next because we will parse again in there + except AttributeError: + pass + + args_dict = vars(args) + used_args = { + key: args_dict[key] + for key + in filter(lambda key: args_dict[key] is not None, args_dict) + } + logger.debug(f'Used argument(s): {used_args}') + logger.debug(f'Unknown argument(s): {unknownArgs}') + + # Report unknown arguments but not if we run aliasParser next because we + # will parse again in there. if unknownArgs and not ('func' in args and args.func is aliasParser): - mainParser.error('Unknown Argument(s): %s' % ', '.join(unknownArgs)) + mainParser.error(f'Unknown argument(s): {unknownArgs}') + return args def printHeader(): diff --git a/common/config.py b/common/config.py index 1d01f52a6..f7776784a 100644 --- a/common/config.py +++ b/common/config.py @@ -176,6 +176,7 @@ def __init__(self, config_path=None, data_path=None): tools.makeDirs(self._LOCAL_MOUNT_ROOT) self._DEFAULT_CONFIG_PATH = os.path.join(self._LOCAL_CONFIG_FOLDER, 'config') + if config_path is None: self._LOCAL_CONFIG_PATH = self._DEFAULT_CONFIG_PATH else: diff --git a/common/diagnostics.py b/common/diagnostics.py index 8fcb74731..9b476a1cc 100644 --- a/common/diagnostics.py +++ b/common/diagnostics.py @@ -4,7 +4,6 @@ paths, operating system and the like. This is used to enhance error reports and to enrich them with the necessary information as uncomplicated as possible. """ - import sys import os import itertools @@ -20,6 +19,24 @@ import version +def collect_minimal_diagnostics(): + """Collect minimal information about backintime and the operating system. + + Returns: + dict: A nested dictionary. + """ + return { + 'backintime': { + 'name': config.Config.APP_NAME, + 'version': version.__version__, + 'running-as-root': pwd.getpwuid(os.getuid()) == 'root' + }, + 'host-setup': { + 'OS': _get_os_release() + } + } + + def collect_diagnostics(): """Collect information about environment, versions of tools and packages used by Back In Time. @@ -29,9 +46,7 @@ def collect_diagnostics(): Returns: dict: A nested dictionary. """ - result = {} - - pwd_struct = pwd.getpwuid(os.getuid()) + result = collect_minimal_diagnostics() # === BACK IN TIME === @@ -39,9 +54,7 @@ def collect_diagnostics(): # (should be singleton) cfg = config.Config() - result['backintime'] = { - 'name': config.Config.APP_NAME, - 'version': version.__version__, + result['backintime'].update({ 'latest-config-version': config.Config.CONFIG_VERSION, 'local-config-file': cfg._LOCAL_CONFIG_PATH, 'local-config-file-found': Path(cfg._LOCAL_CONFIG_PATH).exists(), @@ -49,10 +62,9 @@ def collect_diagnostics(): 'global-config-file-found': Path(cfg._GLOBAL_CONFIG_PATH).exists(), # 'distribution-package': str(distro_path), 'started-from': str(Path(config.__file__).parent), - 'running-as-root': pwd_struct.pw_name == 'root', 'user-callback': cfg.takeSnapshotUserCallback(), 'keyring-supported': tools.keyringSupported() - } + }) # Git repo bit_root_path = Path(tools.backintimePath("")) @@ -66,15 +78,12 @@ def collect_diagnostics(): result['backintime'][f'git-{key}'] = git_info[key] # == HOST setup === - result['host-setup'] = { + result['host-setup'].update({ # Kernel & Architecture 'platform': platform.platform(), # OS Version (and maybe name) - 'system': '{} {}'.format(platform.system(), platform.version()), - # OS Release name (prettier) - 'OS': _get_os_release() - - } + 'system': f'{platform.system()} {platform.version()}' + }) # Display system (X11 or Wayland) # This doesn't catch all edge cases. @@ -101,20 +110,21 @@ def collect_diagnostics(): result['host-setup'][var] = os.environ.get(var, '(not set)') # === PYTHON setup === - python = '{} {} {} {}'.format( + python = ' '.join(( platform.python_version(), ' '.join(platform.python_build()), platform.python_implementation(), platform.python_compiler() - ) + )) # Python branch and revision if available branch = platform.python_branch() if branch: - python = '{} branch: {}'.format(python, branch) + python = f'{python} branch: {branch}' + rev = platform.python_revision() if rev: - python = '{} rev: {}'.format(python, rev) + python = f'{python} rev: {rev}' python_executable = Path(sys.executable) @@ -190,8 +200,10 @@ def collect_diagnostics(): result['external-programs']['shell-version'] \ = shell_version.split('\n')[0] - result = _replace_username_paths(result=result, - username=pwd_struct.pw_name) + result = _replace_username_paths( + result=result, + username=pwd.getpwuid(os.getuid()).pw_name + ) return result @@ -202,6 +214,7 @@ def _get_qt_information(): If environment variable ``DISPLAY`` is set a temporary QApplication instances is created. """ + # pylint: disable=import-outside-toplevel try: import PyQt6.QtCore import PyQt6.QtGui @@ -223,8 +236,8 @@ def _get_qt_information(): qapp.quit() return { - 'Version': 'PyQt {} / Qt {}'.format(PyQt6.QtCore.PYQT_VERSION_STR, - PyQt6.QtCore.QT_VERSION_STR), + 'Version': f'PyQt {PyQt6.QtCore.PYQT_VERSION_STR} ' + f'/ Qt {PyQt6.QtCore.QT_VERSION_STR}', **theme_info } diff --git a/common/plugins/usercallbackplugin.py b/common/plugins/usercallbackplugin.py index edb8ab1fd..69831409e 100644 --- a/common/plugins/usercallbackplugin.py +++ b/common/plugins/usercallbackplugin.py @@ -68,7 +68,7 @@ def init(self, snapshots): return os.path.exists(self.script) # TODO 09/28/2022: This method should be private (_callback) - def callback(self, *args, profileID = None): + def callback(self, *args, profileID=None): if profileID is None: profileID = self.config.currentProfile() diff --git a/common/qt_probing.py b/common/qt_probing.py index b46383200..fe3a1cede 100644 --- a/common/qt_probing.py +++ b/common/qt_probing.py @@ -101,9 +101,8 @@ # os.seteuid(1000) # logger.debug(f"New euid: {os.geteuid()}") - # Disable pylint "import-error" because of TravisCI ppc64le architecture - from PyQt6 import QtCore # pylint: disable=import-error - from PyQt6.QtWidgets import QApplication # pylint: disable=import-error + from PyQt6 import QtCore + from PyQt6.QtWidgets import QApplication app = QApplication(['']) @@ -118,7 +117,7 @@ # ("GUI") is active at all (e.g. in headless installations it isn't). # See: https://forum.qt.io/topic/3852/issystemtrayavailable-always-crashes-segfault-on-ubuntu-10-10-desktop/6 - from PyQt6.QtWidgets import QSystemTrayIcon # pylint: disable=import-error + from PyQt6.QtWidgets import QSystemTrayIcon is_sys_tray_available = QSystemTrayIcon.isSystemTrayAvailable() if is_sys_tray_available: diff --git a/common/test/test_diagnostics.py b/common/test/test_diagnostics.py index 3d947514c..e611d1664 100644 --- a/common/test/test_diagnostics.py +++ b/common/test/test_diagnostics.py @@ -1,3 +1,4 @@ +"""Test related to diagnostics.py""" import sys import pathlib import unittest @@ -8,15 +9,31 @@ class Diagnostics(unittest.TestCase): + """Test about collecting diagnostic infos.""" - def test_minimal(self): + def test_content_minimal(self): """Minimal set of elements.""" + sut = diagnostics.collect_minimal_diagnostics() + + # 1st level keys + self.assertCountEqual(sut.keys(), ['backintime', 'host-setup']) + + # 2nd level "backintime" + self.assertCountEqual( + sut['backintime'].keys(), + ['name', 'version', 'running-as-root']) + + # 2nd level "host-setup" + self.assertCountEqual(sut['host-setup'].keys(), ['OS']) + + def test_some_content(self): + """Some containted elements""" result = diagnostics.collect_diagnostics() # 1st level keys - self.assertEqual( - sorted(result.keys()), + self.assertCountEqual( + result.keys(), ['backintime', 'external-programs', 'host-setup', 'python-setup'] ) @@ -59,8 +76,7 @@ def test_no_ressource_warning(self): diagnostics.collect_diagnostics() def test_no_extern_version(self): - """Get version from not existing tool. - """ + """Get version from not existing tool.""" self.assertEqual( diagnostics._get_extern_versions(['fooXbar']), '(no fooXbar)' @@ -68,7 +84,6 @@ def test_no_extern_version(self): def test_replace_user_path(self): """Replace users path.""" - d = { 'foo': '/home/rsync', 'bar': '~/rsync' @@ -86,5 +101,3 @@ def test_replace_user_path(self): diagnostics._replace_username_paths(d, 'user'), d ) - - diff --git a/common/test/test_lint.py b/common/test/test_lint.py index 5213da0d7..c85503bef 100644 --- a/common/test/test_lint.py +++ b/common/test/test_lint.py @@ -9,6 +9,7 @@ PYLINT_AVIALBE = not shutil.which('pylint') is None PYLINT_REASON = ('Using PyLint is mandatory on TravisCI, on other systems' 'it runs only if `pylint` is available.') +ON_TRAVIS_PPC64LE = os.environ.get('TRAVIS_ARCH', '') == 'ppc64le' class MirrorMirrorOnTheWall(unittest.TestCase): @@ -51,8 +52,10 @@ def test_with_pylint(self): # Pylint base command cmd = [ 'pylint', - # Make sure BIT modules can be improted (to detect "no-member") - '--init-hook=import sys;sys.path.insert(0, "./../qt");', + # Make sure BIT modules can be imported (to detect "no-member") + '--init-hook=import sys;' + 'sys.path.insert(0, "./../qt");' + 'sys.path.insert(0, "./../common");', # Storing results in a pickle file is unnecessary '--persistent=n', # autodetec number of parallel jobs @@ -79,9 +82,26 @@ def test_with_pylint(self): 'E0401', # import-error 'I0021', # useless-suppression ] + + if ON_TRAVIS_PPC64LE: + # Because of missing PyQt6 on ppc64le architecture + err_codes.remove('I0021') + err_codes.remove('E0401') + cmd.append('--enable=' + ','.join(err_codes)) # Add py files cmd.extend(self._collect_py_files()) - subprocess.run(cmd, check=True) + r = subprocess.run( + cmd, + check=False, + universal_newlines=True, + capture_output=True) + + # Count lines except module headings + error_n = len(list(filter(lambda line: not line.startswith('*****'), + r.stdout.splitlines()))) + print(r.stdout) + + self.assertEqual(0, error_n, f'PyLint found {error_n} problems.') diff --git a/common/test/test_plugin_usercallback.py b/common/test/test_plugin_usercallback.py index ce2c6a1cf..b0ae9ef0e 100644 --- a/common/test/test_plugin_usercallback.py +++ b/common/test/test_plugin_usercallback.py @@ -14,6 +14,7 @@ sys.path.append(str(Path(__file__).parent)) sys.path.append(str(Path(__file__).parent / 'plugins')) import logger +import pluginmanager from config import Config from snapshots import Snapshots, SID from usercallbackplugin import UserCallbackPlugin @@ -131,8 +132,7 @@ def _create_user_callback_file(cls, parent_path): content = inspect.cleandoc(''' #!/usr/bin/env python3 import sys - response = sys.argv[1:] - print(response) + print(sys.argv[1:]) ''') callback_fp = parent_path / 'user-callback' @@ -179,6 +179,8 @@ def _extract_callback_responses(cls, output): callback_responses = [] for line in response_lines: + to_eval = line[line.index("'")+1:line.rindex("'")] + callback_responses.append( eval(line[line.index("'")+1:line.rindex("'")]) ) @@ -252,6 +254,9 @@ def setUp(self): self.config_fp = self._create_config_file(self.temp_path) self._create_user_callback_file(self.config_fp.parent) + # Reset this instance because it is not isolated between tests. + Config.PLUGIN_MANAGER = pluginmanager.PluginManager() + def test_local_snapshot(self): """User-callback response while doing a local snapshot""" diff --git a/common/tools.py b/common/tools.py index 768f52c01..44aeb371a 100644 --- a/common/tools.py +++ b/common/tools.py @@ -53,9 +53,9 @@ # because the latter is still not available here in the global # module code. if os.getenv('BIT_USE_KEYRING', 'true') == 'true' and os.geteuid() != 0: - import keyring # pylint: disable=import-error - from keyring import backend # pylint: disable=import-error - import keyring.util.platform_ # pylint: disable=import-error + import keyring + from keyring import backend + import keyring.util.platform_ is_keyring_available = True except Exception as e: is_keyring_available = False diff --git a/qt/aboutdlg.py b/qt/aboutdlg.py index 1602a1571..4e86b8903 100644 --- a/qt/aboutdlg.py +++ b/qt/aboutdlg.py @@ -29,7 +29,7 @@ import backintime -class AboutDlg(QDialog): # pylint: disable=too-few-public-methods +class AboutDlg(QDialog): """The about dialog accessible from the Help menu in the main window.""" def __init__(self, parent=None): diff --git a/qt/languagedialog.py b/qt/languagedialog.py index 556c60d6b..3af759411 100644 --- a/qt/languagedialog.py +++ b/qt/languagedialog.py @@ -169,7 +169,6 @@ def slot_radio(self, _): btn = self.sender() if btn.isChecked(): - # pylint: disable-next=attribute-defined-outside-init self.language_code = btn.lang_code @@ -268,7 +267,7 @@ def _fix_size(self): if self.height() < best.height(): self.resize(best) - def resizeEvent(self, event): # pylint: disable=invalid-name + def resizeEvent(self, event): """See `_fixSize()` for details.""" super().resizeEvent(event) diff --git a/qt/logviewdialog.py b/qt/logviewdialog.py index 21f5495d7..364d4fcdd 100644 --- a/qt/logviewdialog.py +++ b/qt/logviewdialog.py @@ -31,7 +31,6 @@ class LogViewDialog(QDialog): # Workaround because of *-imports of Qt elements. # Remove as soon as possible. - # pylint: disable=undefined-variable def __init__(self, parent, sid = None, systray = False): """ Instantiate a snapshot log file viewer diff --git a/qt/serviceHelper.py b/qt/serviceHelper.py index fd9cc6bdc..cb28ad06d 100644 --- a/qt/serviceHelper.py +++ b/qt/serviceHelper.py @@ -71,11 +71,13 @@ # "dbus-python" not available for ppc64le architecture # "dbus.mainloop.pyqt6" not available via PyPi for any architecture -import dbus # pylint: disable=import-error -import dbus.service # pylint: disable=import-error -import dbus.mainloop # pylint: disable=import-error -import dbus.mainloop.pyqt6 # pylint: disable=import-error -from dbus.mainloop.pyqt6 import DBusQtMainLoop # pylint: disable=import-error +import dbus +import dbus.service +import dbus.mainloop +# pylint: disable-next=import-error,useless-suppression +import dbus.mainloop.pyqt6 +# pylint: disable-next=import-error,useless-suppression +from dbus.mainloop.pyqt6 import DBusQtMainLoop from PyQt6.QtCore import QCoreApplication UDEV_RULES_PATH = '/etc/udev/rules.d/99-backintime-%s.rules' diff --git a/qt/test/test_lint.py b/qt/test/test_lint.py index 781f4a7f2..c85503bef 100644 --- a/qt/test/test_lint.py +++ b/qt/test/test_lint.py @@ -9,6 +9,7 @@ PYLINT_AVIALBE = not shutil.which('pylint') is None PYLINT_REASON = ('Using PyLint is mandatory on TravisCI, on other systems' 'it runs only if `pylint` is available.') +ON_TRAVIS_PPC64LE = os.environ.get('TRAVIS_ARCH', '') == 'ppc64le' class MirrorMirrorOnTheWall(unittest.TestCase): @@ -51,8 +52,10 @@ def test_with_pylint(self): # Pylint base command cmd = [ 'pylint', - # Workaround - '--init-hook=import sys;sys.path.insert(0, "./../common");', + # Make sure BIT modules can be imported (to detect "no-member") + '--init-hook=import sys;' + 'sys.path.insert(0, "./../qt");' + 'sys.path.insert(0, "./../common");', # Storing results in a pickle file is unnecessary '--persistent=n', # autodetec number of parallel jobs @@ -79,9 +82,26 @@ def test_with_pylint(self): 'E0401', # import-error 'I0021', # useless-suppression ] + + if ON_TRAVIS_PPC64LE: + # Because of missing PyQt6 on ppc64le architecture + err_codes.remove('I0021') + err_codes.remove('E0401') + cmd.append('--enable=' + ','.join(err_codes)) # Add py files cmd.extend(self._collect_py_files()) - subprocess.run(cmd, check=True) + r = subprocess.run( + cmd, + check=False, + universal_newlines=True, + capture_output=True) + + # Count lines except module headings + error_n = len(list(filter(lambda line: not line.startswith('*****'), + r.stdout.splitlines()))) + print(r.stdout) + + self.assertEqual(0, error_n, f'PyLint found {error_n} problems.')