diff --git a/openlp/core/__init__.py b/openlp/core/__init__.py index af29cddae..88b3dbfb7 100644 --- a/openlp/core/__init__.py +++ b/openlp/core/__init__.py @@ -33,13 +33,14 @@ import os import shutil import sys import time -from pathlib import Path +from datetime import datetime from traceback import format_exception from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import Registry, OpenLPMixin, AppLocation, LanguageManager, Settings, UiStrings, \ check_directory_exists, is_macosx, is_win, translate +from openlp.core.common.path import Path from openlp.core.common.versionchecker import VersionThread, get_application_version from openlp.core.lib import ScreenList from openlp.core.resources import qInitResources @@ -347,8 +348,7 @@ def set_up_logging(log_path): """ Setup our logging using log_path - :param pathlib.Path log_path: The file to save the log to - :return: None + :param openlp.core.common.path.Path log_path: The file to save the log to. :rtype: None """ check_directory_exists(log_path, True) @@ -406,7 +406,7 @@ def main(args=None): # Set our data path log.info('Data path: {name}'.format(name=data_path)) # Point to our data path - portable_settings.setValue('advanced/data path', str(data_path)) + portable_settings.setValue('advanced/data path', data_path) portable_settings.setValue('advanced/is portable', True) portable_settings.sync() else: @@ -423,8 +423,21 @@ def main(args=None): if application.is_data_path_missing(): application.shared_memory.detach() sys.exit() - # Remove/convert obsolete settings. - Settings().remove_obsolete_settings() + # Upgrade settings. + settings = Settings() + if settings.can_upgrade(): + now = datetime.now() + # Only back up if OpenLP has previously run. + if settings.value('core/has run wizard'): + back_up_path = AppLocation.get_data_path() / (now.strftime('%Y-%m-%d %H-%M') + '.conf') + log.info('Settings about to be upgraded. Existing settings are being backed up to {back_up_path}' + .format(back_up_path=back_up_path)) + QtWidgets.QMessageBox.information( + None, translate('OpenLP', 'Settings Upgrade'), + translate('OpenLP', 'Your settings are about to upgraded. A backup will be created at {back_up_path}') + .format(back_up_path=back_up_path)) + settings.export(back_up_path) + settings.upgrade_settings() # First time checks in settings if not Settings().value('core/has run wizard'): if not FirstTimeLanguageForm().exec(): diff --git a/openlp/core/api/endpoint/controller.py b/openlp/core/api/endpoint/controller.py index 85e9623f9..c3f5fd89f 100644 --- a/openlp/core/api/endpoint/controller.py +++ b/openlp/core/api/endpoint/controller.py @@ -79,8 +79,7 @@ def controller_text(request): item['title'] = str(frame['display_title']) if current_item.is_capable(ItemCapabilities.HasNotes): item['slide_notes'] = str(frame['notes']) - if current_item.is_capable(ItemCapabilities.HasThumbnails) and \ - Settings().value('api/thumbnails'): + if current_item.is_capable(ItemCapabilities.HasThumbnails) and Settings().value('api/thumbnails'): # If the file is under our app directory tree send the portion after the match data_path = str(AppLocation.get_data_path()) if frame['image'][0:len(data_path)] == data_path: diff --git a/openlp/core/common/__init__.py b/openlp/core/common/__init__.py index 632ef0de3..9a4a1935b 100644 --- a/openlp/core/common/__init__.py +++ b/openlp/core/common/__init__.py @@ -66,9 +66,8 @@ def check_directory_exists(directory, do_not_log=False): """ Check a directory exists and if not create it - :param pathlib.Path directory: The directory to make sure exists + :param openlp.core.common.path.Path directory: The directory to make sure exists :param bool do_not_log: To not log anything. This is need for the start up, when the log isn't ready. - :return: None :rtype: None """ if not do_not_log: @@ -89,7 +88,6 @@ def extension_loader(glob_pattern, excluded_files=[]): :param str glob_pattern: A glob pattern used to find the extension(s) to be imported. Should be relative to the application directory. i.e. plugins/*/*plugin.py :param list[str] excluded_files: A list of file names to exclude that the glob pattern may find. - :return: None :rtype: None """ app_dir = AppLocation.get_directory(AppLocation.AppDir) @@ -110,7 +108,7 @@ def path_to_module(path): """ Convert a path to a module name (i.e openlp.core.common) - :param pathlib.Path path: The path to convert to a module name. + :param openlp.core.common.path.Path path: The path to convert to a module name. :return: The module name. :rtype: str """ @@ -377,7 +375,7 @@ def delete_file(file_path): """ Deletes a file from the system. - :param pathlib.Path file_path: The file, including path, to delete. + :param openlp.core.common.path.Path file_path: The file, including path, to delete. :return: True if the deletion was successful, or the file never existed. False otherwise. :rtype: bool """ @@ -412,7 +410,7 @@ def is_not_image_file(file_path): """ Validate that the file is not an image file. - :param pathlib.Path file_path: The file to be checked. + :param openlp.core.common.path.Path file_path: The file to be checked. :return: If the file is not an image :rtype: bool """ @@ -440,7 +438,7 @@ def check_binary_exists(program_path): """ Function that checks whether a binary exists. - :param pathlib.Path program_path: The full path to the binary to check. + :param openlp.core.common.path.Path program_path: The full path to the binary to check. :return: program output to be parsed :rtype: bytes """ @@ -466,7 +464,7 @@ def get_file_encoding(file_path): """ Utility function to incrementally detect the file encoding. - :param pathlib.Path file_path: Filename for the file to determine the encoding for. + :param openlp.core.common.path.Path file_path: Filename for the file to determine the encoding for. :return: A dict with the keys 'encoding' and 'confidence' :rtype: dict[str, float] """ diff --git a/openlp/core/common/applocation.py b/openlp/core/common/applocation.py index 3bcdc9cb9..87ef7e6c1 100644 --- a/openlp/core/common/applocation.py +++ b/openlp/core/common/applocation.py @@ -25,9 +25,9 @@ The :mod:`openlp.core.common.applocation` module provides an utility for OpenLP import logging import os import sys -from pathlib import Path from openlp.core.common import Settings, is_win, is_macosx +from openlp.core.common.path import Path if not is_win() and not is_macosx(): @@ -64,10 +64,8 @@ class AppLocation(object): Return the appropriate directory according to the directory type. :param dir_type: The directory type you want, for instance the data directory. Default *AppLocation.AppDir* - :type dir_type: AppLocation Enum - :return: The requested path - :rtype: pathlib.Path + :rtype: openlp.core.common.path.Path """ if dir_type == AppLocation.AppDir or dir_type == AppLocation.VersionDir: return get_frozen_path(FROZEN_APP_PATH, APP_PATH) @@ -84,11 +82,11 @@ class AppLocation(object): Return the path OpenLP stores all its data under. :return: The data path to use. - :rtype: pathlib.Path + :rtype: openlp.core.common.path.Path """ # Check if we have a different data location. if Settings().contains('advanced/data path'): - path = Path(Settings().value('advanced/data path')) + path = Settings().value('advanced/data path') else: path = AppLocation.get_directory(AppLocation.DataDir) check_directory_exists(path) @@ -104,7 +102,7 @@ class AppLocation(object): :param str extension: Defaults to ''. The extension to search for. For example:: '.png' :return: List of files found. - :rtype: list[pathlib.Path] + :rtype: list[openlp.core.common.path.Path] """ path = AppLocation.get_data_path() if section: @@ -120,9 +118,8 @@ class AppLocation(object): """ Return the path a particular module stores its data under. - :type section: str - - :rtype: pathlib.Path + :param str section: + :rtype: openlp.core.common.path.Path """ path = AppLocation.get_data_path() / section check_directory_exists(path) @@ -135,7 +132,7 @@ def _get_os_dir_path(dir_type): :param dir_type: AppLocation Enum of the requested path type :return: The requested path - :rtype: pathlib.Path + :rtype: openlp.core.common.path.Path """ # If running from source, return the language directory from the source directory if dir_type == AppLocation.LanguageDir: diff --git a/openlp/core/common/json.py b/openlp/core/common/json.py new file mode 100644 index 000000000..d25add283 --- /dev/null +++ b/openlp/core/common/json.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2017 OpenLP Developers # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +from json import JSONDecoder, JSONEncoder + +from openlp.core.common.path import Path + + +class OpenLPJsonDecoder(JSONDecoder): + """ + Implement a custom JSONDecoder to handle Path objects + + Example Usage: + object = json.loads(json_string, cls=OpenLPJsonDecoder) + """ + def __init__(self, object_hook=None, parse_float=None, parse_int=None, parse_constant=None, strict=True, + object_pairs_hook=None, **kwargs): + """ + Re-implement __init__ so that we can pass in our object_hook method. Any additional kwargs, are stored in the + instance and are passed to custom objects upon encoding or decoding. + """ + self.kwargs = kwargs + if object_hook is None: + object_hook = self.custom_object_hook + super().__init__(object_hook, parse_float, parse_int, parse_constant, strict, object_pairs_hook) + + def custom_object_hook(self, obj): + """ + Implement a custom Path object decoder. + + :param dict obj: A decoded JSON object + :return: The original object literal, or a Path object if the object literal contains a key '__Path__' + :rtype: dict | openlp.core.common.path.Path + """ + if '__Path__' in obj: + obj = Path.encode_json(obj, **self.kwargs) + return obj + + +class OpenLPJsonEncoder(JSONEncoder): + """ + Implement a custom JSONEncoder to handle Path objects + + Example Usage: + json_string = json.dumps(object, cls=OpenLPJsonEncoder) + """ + def __init__(self, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, sort_keys=False, + indent=None, separators=None, default=None, **kwargs): + """ + Re-implement __init__ so that we can pass in additional kwargs, which are stored in the instance and are passed + to custom objects upon encoding or decoding. + """ + self.kwargs = kwargs + if default is None: + default = self.custom_default + super().__init__(skipkeys, ensure_ascii, check_circular, allow_nan, sort_keys, indent, separators, default) + + def custom_default(self, obj): + """ + Convert any Path objects into a dictionary object which can be serialized. + + :param object obj: The object to convert + :return: The serializable object + :rtype: dict + """ + if isinstance(obj, Path): + return obj.json_object(**self.kwargs) + return super().default(obj) diff --git a/openlp/core/common/path.py b/openlp/core/common/path.py index 12bef802d..3c4dd93c9 100644 --- a/openlp/core/common/path.py +++ b/openlp/core/common/path.py @@ -19,17 +19,21 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### +from contextlib import suppress -from pathlib import Path +from openlp.core.common import is_win + +if is_win(): + from pathlib import WindowsPath as PathVariant +else: + from pathlib import PosixPath as PathVariant -def path_to_str(path): +def path_to_str(path=None): """ A utility function to convert a Path object or NoneType to a string equivalent. - :param path: The value to convert to a string - :type: pathlib.Path or None - + :param openlp.core.common.path.Path | None path: The value to convert to a string :return: An empty string if :param:`path` is None, else a string representation of the :param:`path` :rtype: str """ @@ -48,14 +52,49 @@ def str_to_path(string): This function is of particular use because initating a Path object with an empty string causes the Path object to point to the current working directory. - :param string: The string to convert - :type string: str - + :param str string: The string to convert :return: None if :param:`string` is empty, or a Path object representation of :param:`string` - :rtype: pathlib.Path or None + :rtype: openlp.core.common.path.Path | None """ if not isinstance(string, str): raise TypeError('parameter \'string\' must be of type str') if string == '': return None return Path(string) + + +class Path(PathVariant): + """ + Subclass pathlib.Path, so we can add json conversion methods + """ + @staticmethod + def encode_json(obj, base_path=None, **kwargs): + """ + Create a Path object from a dictionary representation. The dictionary has been constructed by JSON encoding of + a JSON reprensation of a Path object. + + :param dict[str] obj: The dictionary representation + :param openlp.core.common.path.Path base_path: If specified, an absolute path to base the relative path off of. + :param kwargs: Contains any extra parameters. Not used! + :return: The reconstructed Path object + :rtype: openlp.core.common.path.Path + """ + path = Path(*obj['__Path__']) + if base_path and not path.is_absolute(): + return base_path / path + return path + + def json_object(self, base_path=None, **kwargs): + """ + Create a dictionary that can be JSON decoded. + + :param openlp.core.common.path.Path base_path: If specified, an absolute path to make a relative path from. + :param kwargs: Contains any extra parameters. Not used! + :return: The dictionary representation of this Path object. + :rtype: dict[tuple] + """ + path = self + if base_path: + with suppress(ValueError): + path = path.relative_to(base_path) + return {'__Path__': path.parts} diff --git a/openlp/core/common/settings.py b/openlp/core/common/settings.py index 8d12a69e5..5bfcd31a9 100644 --- a/openlp/core/common/settings.py +++ b/openlp/core/common/settings.py @@ -24,15 +24,19 @@ This class contains the core default settings. """ import datetime import logging +import json import os +from tempfile import gettempdir -from PyQt5 import QtCore, QtGui - -from openlp.core.common import ThemeLevel, SlideLimits, UiStrings, is_win, is_linux +from PyQt5 import QtCore, QtGui, QtWidgets +from openlp.core.common import SlideLimits, ThemeLevel, UiStrings, is_linux, is_win, translate +from openlp.core.common.json import OpenLPJsonDecoder, OpenLPJsonEncoder +from openlp.core.common.path import Path, str_to_path log = logging.getLogger(__name__) +__version__ = 2 # Fix for bug #1014422. X11_BYPASS_DEFAULT = True @@ -44,21 +48,6 @@ if is_linux(): X11_BYPASS_DEFAULT = False -def recent_files_conv(value): - """ - If the value is not a list convert it to a list - :param value: Value to convert - :return: value as a List - """ - if isinstance(value, list): - return value - elif isinstance(value, str): - return [value] - elif isinstance(value, bytes): - return [value.decode()] - return [] - - def media_players_conv(string): """ If phonon is in the setting string replace it with system @@ -73,14 +62,25 @@ def media_players_conv(string): return string +def file_names_conv(file_names): + """ + Convert a list of file names in to a list of file paths. + + :param list[str] file_names: The list of file names to convert. + :return: The list converted to file paths + :rtype: openlp.core.common.path.Path + """ + if file_names: + return [str_to_path(file_name) for file_name in file_names] + + class Settings(QtCore.QSettings): """ Class to wrap QSettings. * Exposes all the methods of QSettings. - * Adds functionality for OpenLP Portable. If the ``defaultFormat`` is set to - ``IniFormat``, and the path to the Ini file is set using ``set_filename``, - then the Settings constructor (without any arguments) will create a Settings + * Adds functionality for OpenLP Portable. If the ``defaultFormat`` is set to ``IniFormat``, and the path to the Ini + file is set using ``set_filename``, then the Settings constructor (without any arguments) will create a Settings object for accessing settings stored in that Ini file. ``__default_settings__`` @@ -91,7 +91,7 @@ class Settings(QtCore.QSettings): ('general/enable slide loop', 'advanced/slide limits', [(SlideLimits.Wrap, True), (SlideLimits.End, False)]) - The first entry is the *old key*; it will be removed. + The first entry is the *old key*; if it is different from the *new key* it will be removed. The second entry is the *new key*; we will add it to the config. If this is just an empty string, we just remove the old key. The last entry is a list containing two-pair tuples. If the list is empty, no conversion is made. @@ -105,11 +105,12 @@ class Settings(QtCore.QSettings): So, if the type of the old value is bool, then there must be two rules. """ __default_settings__ = { + 'settings/version': 0, 'advanced/add page break': False, 'advanced/alternate rows': not is_win(), 'advanced/autoscrolling': {'dist': 1, 'pos': 0}, 'advanced/current media plugin': -1, - 'advanced/data path': '', + 'advanced/data path': None, # 7 stands for now, 0 to 6 is Monday to Sunday. 'advanced/default service day': 7, 'advanced/default service enabled': True, @@ -143,7 +144,7 @@ class Settings(QtCore.QSettings): 'api/authentication enabled': False, 'api/ip address': '0.0.0.0', 'api/thumbnails': True, - 'crashreport/last directory': '', + 'crashreport/last directory': None, 'formattingTags/html_tags': '', 'core/audio repeat list': False, 'core/auto open': False, @@ -162,7 +163,7 @@ class Settings(QtCore.QSettings): 'core/screen blank': False, 'core/show splash': True, 'core/logo background color': '#ffffff', - 'core/logo file': ':/graphics/openlp-splash-screen.png', + 'core/logo file': Path(':/graphics/openlp-splash-screen.png'), 'core/logo hide on startup': False, 'core/songselect password': '', 'core/songselect username': '', @@ -177,17 +178,17 @@ class Settings(QtCore.QSettings): 'media/players': 'system,webkit', 'media/override player': QtCore.Qt.Unchecked, 'players/background color': '#000000', - 'servicemanager/last directory': '', - 'servicemanager/last file': '', - 'servicemanager/service theme': '', + 'servicemanager/last directory': None, + 'servicemanager/last file': None, + 'servicemanager/service theme': None, 'SettingsImport/file_date_created': datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), 'SettingsImport/Make_Changes': 'At_Own_RISK', 'SettingsImport/type': 'OpenLP_settings_export', 'SettingsImport/version': '', 'themes/global theme': '', - 'themes/last directory': '', - 'themes/last directory export': '', - 'themes/last directory import': '', + 'themes/last directory': None, + 'themes/last directory export': None, + 'themes/last directory import': None, 'themes/theme level': ThemeLevel.Song, 'themes/wrap footer': False, 'user interface/live panel': True, @@ -208,22 +209,20 @@ class Settings(QtCore.QSettings): 'projector/db database': '', 'projector/enable': True, 'projector/connect on start': False, - 'projector/last directory import': '', - 'projector/last directory export': '', + 'projector/last directory import': None, + 'projector/last directory export': None, 'projector/poll time': 20, # PJLink timeout is 30 seconds 'projector/socket timeout': 5, # 5 second socket timeout 'projector/source dialog type': 0 # Source select dialog box type } __file_path__ = '' - __obsolete_settings__ = [ + __setting_upgrade_1__ = [ # Changed during 2.2.x development. - # ('advanced/stylesheet fix', '', []), - # ('general/recent files', 'core/recent files', [(recent_files_conv, None)]), ('songs/search as type', 'advanced/search as type', []), ('media/players', 'media/players_temp', [(media_players_conv, None)]), # Convert phonon to system ('media/players_temp', 'media/players', []), # Move temp setting from above to correct setting ('advanced/default color', 'core/logo background color', []), # Default image renamed + moved to general > 2.4. - ('advanced/default image', '/core/logo file', []), # Default image renamed + moved to general after 2.4. + ('advanced/default image', 'core/logo file', []), # Default image renamed + moved to general after 2.4. ('remotes/https enabled', '', []), ('remotes/https port', '', []), ('remotes/twelve hour', 'api/twelve hour', []), @@ -234,7 +233,6 @@ class Settings(QtCore.QSettings): ('remotes/authentication enabled', 'api/authentication enabled', []), ('remotes/ip address', 'api/ip address', []), ('remotes/thumbnails', 'api/thumbnails', []), - ('advanced/default image', 'core/logo file', []), # Default image renamed + moved to general after 2.4. ('shortcuts/escapeItem', 'shortcuts/desktopScreenEnable', []), # Escape item was removed in 2.6. ('shortcuts/offlineHelpItem', 'shortcuts/userManualItem', []), # Online and Offline help were combined in 2.6. ('shortcuts/onlineHelpItem', 'shortcuts/userManualItem', []), # Online and Offline help were combined in 2.6. @@ -243,7 +241,28 @@ class Settings(QtCore.QSettings): # Last search type was renamed to last used search type in 2.6 since Bible search value type changed in 2.6. ('songs/last search type', 'songs/last used search type', []), ('bibles/last search type', '', []), - ('custom/last search type', 'custom/last used search type', []) + ('custom/last search type', 'custom/last used search type', [])] + + __setting_upgrade_2__ = [ + # The following changes are being made for the conversion to using Path objects made in 2.6 development + ('advanced/data path', 'advanced/data path', [(str_to_path, None)]), + ('crashreport/last directory', 'crashreport/last directory', [(str_to_path, None)]), + ('servicemanager/last directory', 'servicemanager/last directory', [(str_to_path, None)]), + ('servicemanager/last file', 'servicemanager/last file', [(str_to_path, None)]), + ('themes/last directory', 'themes/last directory', [(str_to_path, None)]), + ('themes/last directory export', 'themes/last directory export', [(str_to_path, None)]), + ('themes/last directory import', 'themes/last directory import', [(str_to_path, None)]), + ('projector/last directory import', 'projector/last directory import', [(str_to_path, None)]), + ('projector/last directory export', 'projector/last directory export', [(str_to_path, None)]), + ('bibles/last directory import', 'bibles/last directory import', [(str_to_path, None)]), + ('presentations/pdf_program', 'presentations/pdf_program', [(str_to_path, None)]), + ('songs/last directory import', 'songs/last directory import', [(str_to_path, None)]), + ('songs/last directory export', 'songs/last directory export', [(str_to_path, None)]), + ('songusage/last directory export', 'songusage/last directory export', [(str_to_path, None)]), + ('core/recent files', 'core/recent files', [(file_names_conv, None)]), + ('media/media files', 'media/media files', [(file_names_conv, None)]), + ('presentations/presentations files', 'presentations/presentations files', [(file_names_conv, None)]), + ('core/logo file', 'core/logo file', [(str_to_path, None)]) ] @staticmethod @@ -256,13 +275,16 @@ class Settings(QtCore.QSettings): Settings.__default_settings__.update(default_values) @staticmethod - def set_filename(ini_file): + def set_filename(ini_path): """ Sets the complete path to an Ini file to be used by Settings objects. Does not affect existing Settings objects. + + :param openlp.core.common.path.Path ini_path: ini file path + :rtype: None """ - Settings.__file_path__ = ini_file + Settings.__file_path__ = str(ini_path) @staticmethod def set_up_default_values(): @@ -431,14 +453,28 @@ class Settings(QtCore.QSettings): key = self.group() + '/' + key return Settings.__default_settings__[key] - def remove_obsolete_settings(self): + def can_upgrade(self): + """ + Can / should the settings be upgraded + + :rtype: bool + """ + return __version__ != self.value('settings/version') + + def upgrade_settings(self): """ This method is only called to clean up the config. It removes old settings and it renames settings. See ``__obsolete_settings__`` for more details. """ - for old_key, new_key, rules in Settings.__obsolete_settings__: - # Once removed we don't have to do this again. - if self.contains(old_key): + current_version = self.value('settings/version') + for version in range(current_version, __version__): + version += 1 + upgrade_list = getattr(self, '__setting_upgrade_{version}__'.format(version=version)) + for old_key, new_key, rules in upgrade_list: + # Once removed we don't have to do this again. - Can be removed once fully switched to the versioning + # system. + if not self.contains(old_key): + continue if new_key: # Get the value of the old_key. old_value = super(Settings, self).value(old_key) @@ -457,14 +493,17 @@ class Settings(QtCore.QSettings): old_value = new break self.setValue(new_key, old_value) - self.remove(old_key) + if new_key != old_key: + self.remove(old_key) + self.setValue('settings/version', version) def value(self, key): """ Returns the value for the given ``key``. The returned ``value`` is of the same type as the default value in the *Settings.__default_settings__* dict. - :param key: The key to return the value from. + :param str key: The key to return the value from. + :return: The value stored by the setting. """ # if group() is not empty the group has not been specified together with the key. if self.group(): @@ -474,6 +513,18 @@ class Settings(QtCore.QSettings): setting = super(Settings, self).value(key, default_value) return self._convert_value(setting, default_value) + def setValue(self, key, value): + """ + Reimplement the setValue method to handle Path objects. + + :param str key: The key of the setting to save + :param value: The value to save + :rtype: None + """ + if isinstance(value, Path) or (isinstance(value, list) and value and isinstance(value[0], Path)): + value = json.dumps(value, cls=OpenLPJsonEncoder) + super().setValue(key, value) + def _convert_value(self, setting, default_value): """ This converts the given ``setting`` to the type of the given ``default_value``. @@ -491,8 +542,11 @@ class Settings(QtCore.QSettings): if isinstance(default_value, str): return '' # An empty list saved to the settings results in a None type being returned. - else: + elif isinstance(default_value, list): return [] + elif isinstance(setting, str): + if '__Path__' in setting: + return json.loads(setting, cls=OpenLPJsonDecoder) # Convert the setting to the correct type. if isinstance(default_value, bool): if isinstance(setting, bool): @@ -502,3 +556,59 @@ class Settings(QtCore.QSettings): if isinstance(default_value, int): return int(setting) return setting + + def export(self, dest_path): + """ + Export the settings to file. + + :param openlp.core.common.path.Path dest_path: The file path to create the export file. + :return: Success + :rtype: bool + """ + temp_path = Path(gettempdir(), 'openlp', 'exportConf.tmp') + # Delete old files if found. + if temp_path.exists(): + temp_path.unlink() + if dest_path.exists(): + dest_path.unlink() + self.remove('SettingsImport') + # Get the settings. + keys = self.allKeys() + export_settings = QtCore.QSettings(str(temp_path), Settings.IniFormat) + # Add a header section. + # This is to insure it's our conf file for import. + now = datetime.datetime.now() + # Write INI format using QSettings. + # Write our header. + export_settings.beginGroup('SettingsImport') + export_settings.setValue('Make_Changes', 'At_Own_RISK') + export_settings.setValue('type', 'OpenLP_settings_export') + export_settings.setValue('file_date_created', now.strftime("%Y-%m-%d %H:%M")) + export_settings.endGroup() + # Write all the sections and keys. + for section_key in keys: + # FIXME: We are conflicting with the standard "General" section. + if 'eneral' in section_key: + section_key = section_key.lower() + key_value = super().value(section_key) + if key_value is not None: + export_settings.setValue(section_key, key_value) + export_settings.sync() + # Temp CONF file has been written. Blanks in keys are now '%20'. + # Read the temp file and output the user's CONF file with blanks to + # make it more readable. + try: + with dest_path.open('w') as export_conf_file, temp_path.open('r') as temp_conf: + for file_record in temp_conf: + # Get rid of any invalid entries. + if file_record.find('@Invalid()') == -1: + file_record = file_record.replace('%20', ' ') + export_conf_file.write(file_record) + except OSError as ose: + QtWidgets.QMessageBox.critical(self, translate('OpenLP.MainWindow', 'Export setting error'), + translate('OpenLP.MainWindow', + 'An error occurred while exporting the settings: {err}' + ).format(err=ose.strerror), + QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Ok)) + finally: + temp_path.unlink() diff --git a/openlp/core/lib/__init__.py b/openlp/core/lib/__init__.py index 599876396..08b675000 100644 --- a/openlp/core/lib/__init__.py +++ b/openlp/core/lib/__init__.py @@ -89,7 +89,7 @@ def get_text_file_string(text_file_path): returns False. If there is an error loading the file or the content can't be decoded then the function will return None. - :param pathlib.Path text_file_path: The path to the file. + :param openlp.core.common.path.Path text_file_path: The path to the file. :return: The contents of the file, False if the file does not exist, or None if there is an Error reading or decoding the file. :rtype: str | False | None @@ -610,17 +610,11 @@ def replace_params(args, kwargs, params): """ Apply a transformation function to the specified args or kwargs - :param args: Positional arguments - :type args: (,) - - :param kwargs: Key Word arguments - :type kwargs: dict - + :param tuple args: Positional arguments + :param dict kwargs: Key Word arguments :param params: A tuple of tuples with the position and the key word to replace. - :type params: ((int, str, path_to_str),) - :return: The modified positional and keyword arguments - :rtype: (tuple, dict) + :rtype: tuple[tuple, dict] Usage: diff --git a/openlp/core/lib/mediamanageritem.py b/openlp/core/lib/mediamanageritem.py index 6b04d076b..59a0e4e33 100644 --- a/openlp/core/lib/mediamanageritem.py +++ b/openlp/core/lib/mediamanageritem.py @@ -29,7 +29,7 @@ import re from PyQt5 import QtCore, QtWidgets from openlp.core.common import Registry, RegistryProperties, Settings, UiStrings, translate -from openlp.core.common.path import path_to_str, str_to_path +from openlp.core.common.path import Path, path_to_str, str_to_path from openlp.core.lib import ServiceItem, StringContent, ServiceItemContext from openlp.core.lib.searchedit import SearchEdit from openlp.core.lib.ui import create_widget_action, critical_error_message_box @@ -313,7 +313,7 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties): """ file_paths, selected_filter = FileDialog.getOpenFileNames( self, self.on_new_prompt, - str_to_path(Settings().value(self.settings_section + '/last directory')), + Settings().value(self.settings_section + '/last directory'), self.on_new_file_masks) log.info('New files(s) {file_paths}'.format(file_paths=file_paths)) if file_paths: @@ -377,9 +377,8 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties): self.list_view.clear() self.load_list(full_list, target_group) last_dir = os.path.split(files[0])[0] - Settings().setValue(self.settings_section + '/last directory', last_dir) - Settings().setValue('{section}/{section} files'.format(section=self.settings_section), - self.get_file_list()) + Settings().setValue(self.settings_section + '/last directory', Path(last_dir)) + Settings().setValue('{section}/{section} files'.format(section=self.settings_section), self.get_file_list()) if duplicates_found: critical_error_message_box(UiStrings().Duplicate, translate('OpenLP.MediaManagerItem', @@ -400,13 +399,15 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties): def get_file_list(self): """ Return the current list of files + + :rtype: list[openlp.core.common.path.Path] """ - file_list = [] + file_paths = [] for index in range(self.list_view.count()): list_item = self.list_view.item(index) filename = list_item.data(QtCore.Qt.UserRole) - file_list.append(filename) - return file_list + file_paths.append(str_to_path(filename)) + return file_paths def load_list(self, load_list, target_group): """ diff --git a/openlp/core/lib/path.py b/openlp/core/lib/path.py deleted file mode 100644 index 12bef802d..000000000 --- a/openlp/core/lib/path.py +++ /dev/null @@ -1,61 +0,0 @@ -# -*- coding: utf-8 -*- -# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 - -############################################################################### -# OpenLP - Open Source Lyrics Projection # -# --------------------------------------------------------------------------- # -# Copyright (c) 2008-2017 OpenLP Developers # -# --------------------------------------------------------------------------- # -# This program is free software; you can redistribute it and/or modify it # -# under the terms of the GNU General Public License as published by the Free # -# Software Foundation; version 2 of the License. # -# # -# This program is distributed in the hope that it will be useful, but WITHOUT # -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # -# more details. # -# # -# You should have received a copy of the GNU General Public License along # -# with this program; if not, write to the Free Software Foundation, Inc., 59 # -# Temple Place, Suite 330, Boston, MA 02111-1307 USA # -############################################################################### - -from pathlib import Path - - -def path_to_str(path): - """ - A utility function to convert a Path object or NoneType to a string equivalent. - - :param path: The value to convert to a string - :type: pathlib.Path or None - - :return: An empty string if :param:`path` is None, else a string representation of the :param:`path` - :rtype: str - """ - if not isinstance(path, Path) and path is not None: - raise TypeError('parameter \'path\' must be of type Path or NoneType') - if path is None: - return '' - else: - return str(path) - - -def str_to_path(string): - """ - A utility function to convert a str object to a Path or NoneType. - - This function is of particular use because initating a Path object with an empty string causes the Path object to - point to the current working directory. - - :param string: The string to convert - :type string: str - - :return: None if :param:`string` is empty, or a Path object representation of :param:`string` - :rtype: pathlib.Path or None - """ - if not isinstance(string, str): - raise TypeError('parameter \'string\' must be of type str') - if string == '': - return None - return Path(string) diff --git a/openlp/core/lib/plugin.py b/openlp/core/lib/plugin.py index b06e0fbd4..1b1cddfea 100644 --- a/openlp/core/lib/plugin.py +++ b/openlp/core/lib/plugin.py @@ -150,7 +150,7 @@ class Plugin(QtCore.QObject, RegistryProperties): self.status = PluginStatus.Inactive # Add the default status to the default settings. default_settings[name + '/status'] = PluginStatus.Inactive - default_settings[name + '/last directory'] = '' + default_settings[name + '/last directory'] = None # Append a setting for files in the mediamanager (note not all plugins # which have a mediamanager need this). if media_item_class is not None: diff --git a/openlp/core/ui/exceptionform.py b/openlp/core/ui/exceptionform.py index 349352d92..00a7c89e2 100644 --- a/openlp/core/ui/exceptionform.py +++ b/openlp/core/ui/exceptionform.py @@ -70,9 +70,9 @@ try: except ImportError: VLC_VERSION = '-' -from openlp.core.common import Settings, UiStrings, translate +from openlp.core.common import RegistryProperties, Settings, UiStrings, is_linux, translate from openlp.core.common.versionchecker import get_application_version -from openlp.core.common import RegistryProperties, is_linux +from openlp.core.ui.lib.filedialog import FileDialog from .exceptiondialog import Ui_ExceptionDialog @@ -139,17 +139,17 @@ class ExceptionForm(QtWidgets.QDialog, Ui_ExceptionDialog, RegistryProperties): """ Saving exception log and system information to a file. """ - filename = QtWidgets.QFileDialog.getSaveFileName( + file_path, filter_used = FileDialog.getSaveFileName( self, translate('OpenLP.ExceptionForm', 'Save Crash Report'), Settings().value(self.settings_section + '/last directory'), - translate('OpenLP.ExceptionForm', 'Text files (*.txt *.log *.text)'))[0] - if filename: - filename = str(filename).replace('/', os.path.sep) - Settings().setValue(self.settings_section + '/last directory', os.path.dirname(filename)) + translate('OpenLP.ExceptionForm', 'Text files (*.txt *.log *.text)')) + if file_path: + Settings().setValue(self.settings_section + '/last directory', file_path.parent) opts = self._create_report() report_text = self.report_text.format(version=opts['version'], description=opts['description'], traceback=opts['traceback'], libs=opts['libs'], system=opts['system']) + filename = str(file_path) try: report_file = open(filename, 'w') try: @@ -212,17 +212,16 @@ class ExceptionForm(QtWidgets.QDialog, Ui_ExceptionDialog, RegistryProperties): def on_attach_file_button_clicked(self): """ - Attache files to the bug report e-mail. + Attach files to the bug report e-mail. """ - files, filter_used = QtWidgets.QFileDialog.getOpenFileName(self, - translate('ImagePlugin.ExceptionDialog', - 'Select Attachment'), - Settings().value(self.settings_section + - '/last directory'), - '{text} (*)'.format(text=UiStrings().AllFiles)) - log.info('New files(s) {files}'.format(files=str(files))) - if files: - self.file_attachment = str(files) + file_path, filter_used = \ + FileDialog.getOpenFileName(self, + translate('ImagePlugin.ExceptionDialog', 'Select Attachment'), + Settings().value(self.settings_section + '/last directory'), + '{text} (*)'.format(text=UiStrings().AllFiles)) + log.info('New file {file}'.format(file=file_path)) + if file_path: + self.file_attachment = str(file_path) def __button_state(self, state): """ diff --git a/openlp/core/ui/firsttimeform.py b/openlp/core/ui/firsttimeform.py index 5623855d2..1ae923467 100644 --- a/openlp/core/ui/firsttimeform.py +++ b/openlp/core/ui/firsttimeform.py @@ -30,13 +30,13 @@ import urllib.request import urllib.parse import urllib.error from configparser import ConfigParser, MissingSectionHeaderError, NoOptionError, NoSectionError -from pathlib import Path from tempfile import gettempdir from PyQt5 import QtCore, QtWidgets from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, check_directory_exists, \ translate, clean_button_text, trace_error_handler +from openlp.core.common.path import Path from openlp.core.lib import PluginStatus, build_icon from openlp.core.lib.ui import critical_error_message_box from openlp.core.common.httputils import get_web_page, get_url_file_size, url_get_file, CONNECTION_TIMEOUT diff --git a/openlp/core/ui/generaltab.py b/openlp/core/ui/generaltab.py index dc084eb2b..70a7ddf5f 100644 --- a/openlp/core/ui/generaltab.py +++ b/openlp/core/ui/generaltab.py @@ -23,12 +23,11 @@ The general tab of the configuration dialog. """ import logging -from pathlib import Path from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import Registry, Settings, UiStrings, translate, get_images_filter -from openlp.core.common.path import path_to_str, str_to_path +from openlp.core.common.path import Path, path_to_str, str_to_path from openlp.core.lib import SettingsTab, ScreenList from openlp.core.ui.lib import ColorButton, PathEdit @@ -294,7 +293,7 @@ class GeneralTab(SettingsTab): self.auto_open_check_box.setChecked(settings.value('auto open')) self.show_splash_check_box.setChecked(settings.value('show splash')) self.logo_background_color = settings.value('logo background color') - self.logo_file_path_edit.path = str_to_path(settings.value('logo file')) + self.logo_file_path_edit.path = settings.value('logo file') self.logo_hide_on_startup_check_box.setChecked(settings.value('logo hide on startup')) self.logo_color_button.color = self.logo_background_color self.check_for_updates_check_box.setChecked(settings.value('update check')) @@ -328,7 +327,7 @@ class GeneralTab(SettingsTab): settings.setValue('auto open', self.auto_open_check_box.isChecked()) settings.setValue('show splash', self.show_splash_check_box.isChecked()) settings.setValue('logo background color', self.logo_background_color) - settings.setValue('logo file', path_to_str(self.logo_file_path_edit.path)) + settings.setValue('logo file', self.logo_file_path_edit.path) settings.setValue('logo hide on startup', self.logo_hide_on_startup_check_box.isChecked()) settings.setValue('update check', self.check_for_updates_check_box.isChecked()) settings.setValue('save prompt', self.save_check_service_check_box.isChecked()) diff --git a/openlp/core/ui/lib/filedialog.py b/openlp/core/ui/lib/filedialog.py index bd2bb5109..d4a702e83 100755 --- a/openlp/core/ui/lib/filedialog.py +++ b/openlp/core/ui/lib/filedialog.py @@ -20,11 +20,9 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ Patch the QFileDialog so it accepts and returns Path objects""" -from pathlib import Path - from PyQt5 import QtWidgets -from openlp.core.common.path import path_to_str, str_to_path +from openlp.core.common.path import Path, path_to_str, str_to_path from openlp.core.lib import replace_params @@ -36,7 +34,7 @@ class FileDialog(QtWidgets.QFileDialog): :type parent: QtWidgets.QWidget or None :type caption: str - :type directory: pathlib.Path + :type directory: openlp.core.common.path.Path :type options: QtWidgets.QFileDialog.Options :rtype: tuple[Path, str] """ @@ -55,7 +53,7 @@ class FileDialog(QtWidgets.QFileDialog): :type parent: QtWidgets.QWidget or None :type caption: str - :type directory: pathlib.Path + :type directory: openlp.core.common.path.Path :type filter: str :type initialFilter: str :type options: QtWidgets.QFileDialog.Options @@ -76,7 +74,7 @@ class FileDialog(QtWidgets.QFileDialog): :type parent: QtWidgets.QWidget or None :type caption: str - :type directory: pathlib.Path + :type directory: openlp.core.common.path.Path :type filter: str :type initialFilter: str :type options: QtWidgets.QFileDialog.Options @@ -98,7 +96,7 @@ class FileDialog(QtWidgets.QFileDialog): :type parent: QtWidgets.QWidget or None :type caption: str - :type directory: pathlib.Path + :type directory: openlp.core.common.path.Path :type filter: str :type initialFilter: str :type options: QtWidgets.QFileDialog.Options diff --git a/openlp/core/ui/lib/pathedit.py b/openlp/core/ui/lib/pathedit.py index dd6066931..d5b9272fa 100644 --- a/openlp/core/ui/lib/pathedit.py +++ b/openlp/core/ui/lib/pathedit.py @@ -20,12 +20,11 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### from enum import Enum -from pathlib import Path from PyQt5 import QtCore, QtWidgets from openlp.core.common import UiStrings, translate -from openlp.core.common.path import path_to_str, str_to_path +from openlp.core.common.path import Path, path_to_str, str_to_path from openlp.core.lib import build_icon from openlp.core.ui.lib.filedialog import FileDialog @@ -46,19 +45,11 @@ class PathEdit(QtWidgets.QWidget): """ Initialise the PathEdit widget - :param parent: The parent of the widget. This is just passed to the super method. - :type parent: QWidget or None - - :param dialog_caption: Used to customise the caption in the QFileDialog. - :type dialog_caption: str - - :param default_path: The default path. This is set as the path when the revert button is clicked - :type default_path: pathlib.Path - - :param show_revert: Used to determine if the 'revert button' should be visible. - :type show_revert: bool - - :return: None + :param QtWidget.QWidget | None: The parent of the widget. This is just passed to the super method. + :param str dialog_caption: Used to customise the caption in the QFileDialog. + :param openlp.core.common.path.Path default_path: The default path. This is set as the path when the revert + button is clicked + :param bool show_revert: Used to determine if the 'revert button' should be visible. :rtype: None """ super().__init__(parent) @@ -72,10 +63,7 @@ class PathEdit(QtWidgets.QWidget): def _setup(self, show_revert): """ Set up the widget - :param show_revert: Show or hide the revert button - :type show_revert: bool - - :return: None + :param bool show_revert: Show or hide the revert button :rtype: None """ widget_layout = QtWidgets.QHBoxLayout() @@ -102,7 +90,7 @@ class PathEdit(QtWidgets.QWidget): A property getter method to return the selected path. :return: The selected path - :rtype: pathlib.Path + :rtype: openlp.core.common.path.Path """ return self._path @@ -111,10 +99,7 @@ class PathEdit(QtWidgets.QWidget): """ A Property setter method to set the selected path - :param path: The path to set the widget to - :type path: pathlib.Path - - :return: None + :param openlp.core.common.path.Path path: The path to set the widget to :rtype: None """ self._path = path @@ -138,10 +123,7 @@ class PathEdit(QtWidgets.QWidget): """ A Property setter method to set the path type - :param path_type: The type of path to select - :type path_type: PathType - - :return: None + :param PathType path_type: The type of path to select :rtype: None """ self._path_type = path_type @@ -151,7 +133,6 @@ class PathEdit(QtWidgets.QWidget): """ Called to update the tooltips on the buttons. This is changing path types, and when the widget is initalised - :return: None :rtype: None """ if self._path_type == PathType.Directories: @@ -167,7 +148,6 @@ class PathEdit(QtWidgets.QWidget): Show the QFileDialog and process the input from the user - :return: None :rtype: None """ caption = self.dialog_caption @@ -189,7 +169,6 @@ class PathEdit(QtWidgets.QWidget): Set the new path to the value of the default_path instance variable. - :return: None :rtype: None """ self.on_new_path(self.default_path) @@ -198,7 +177,6 @@ class PathEdit(QtWidgets.QWidget): """ A handler to handle when the line edit has finished being edited. - :return: None :rtype: None """ path = str_to_path(self.line_edit.text()) @@ -210,10 +188,7 @@ class PathEdit(QtWidgets.QWidget): Emits the pathChanged Signal - :param path: The new path - :type path: pathlib.Path - - :return: None + :param openlp.core.common.path.Path path: The new path :rtype: None """ if self._path != path: diff --git a/openlp/core/ui/lib/wizard.py b/openlp/core/ui/lib/wizard.py index 936682585..8f3093fef 100644 --- a/openlp/core/ui/lib/wizard.py +++ b/openlp/core/ui/lib/wizard.py @@ -30,6 +30,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import Registry, RegistryProperties, Settings, UiStrings, translate, is_macosx from openlp.core.lib import build_icon from openlp.core.lib.ui import add_welcome_page +from openlp.core.ui.lib.filedialog import FileDialog log = logging.getLogger(__name__) @@ -278,37 +279,38 @@ class OpenLPWizard(QtWidgets.QWizard, RegistryProperties): def get_file_name(self, title, editbox, setting_name, filters=''): """ - Opens a QFileDialog and saves the filename to the given editbox. + Opens a FileDialog and saves the filename to the given editbox. - :param title: The title of the dialog (unicode). - :param editbox: An editbox (QLineEdit). - :param setting_name: The place where to save the last opened directory. - :param filters: The file extension filters. It should contain the file description + :param str title: The title of the dialog. + :param QtWidgets.QLineEdit editbox: An QLineEdit. + :param str setting_name: The place where to save the last opened directory. + :param str filters: The file extension filters. It should contain the file description as well as the file extension. For example:: 'OpenLP 2 Databases (*.sqlite)' + :rtype: None """ if filters: filters += ';;' filters += '%s (*)' % UiStrings().AllFiles - filename, filter_used = QtWidgets.QFileDialog.getOpenFileName( - self, title, os.path.dirname(Settings().value(self.plugin.settings_section + '/' + setting_name)), - filters) - if filename: - editbox.setText(filename) - Settings().setValue(self.plugin.settings_section + '/' + setting_name, filename) + file_path, filter_used = FileDialog.getOpenFileName( + self, title, Settings().value(self.plugin.settings_section + '/' + setting_name), filters) + if file_path: + editbox.setText(str(file_path)) + Settings().setValue(self.plugin.settings_section + '/' + setting_name, file_path.parent) def get_folder(self, title, editbox, setting_name): """ - Opens a QFileDialog and saves the selected folder to the given editbox. + Opens a FileDialog and saves the selected folder to the given editbox. - :param title: The title of the dialog (unicode). - :param editbox: An editbox (QLineEdit). - :param setting_name: The place where to save the last opened directory. + :param str title: The title of the dialog. + :param QtWidgets.QLineEdit editbox: An QLineEditbox. + :param str setting_name: The place where to save the last opened directory. + :rtype: None """ - folder = QtWidgets.QFileDialog.getExistingDirectory( + folder_path = FileDialog.getExistingDirectory( self, title, Settings().value(self.plugin.settings_section + '/' + setting_name), QtWidgets.QFileDialog.ShowDirsOnly) - if folder: - editbox.setText(folder) - Settings().setValue(self.plugin.settings_section + '/' + setting_name, folder) + if folder_path: + editbox.setText(str(folder_path)) + Settings().setValue(self.plugin.settings_section + '/' + setting_name, folder_path) diff --git a/openlp/core/ui/maindisplay.py b/openlp/core/ui/maindisplay.py index 97f2f7eb5..862fcd0bc 100644 --- a/openlp/core/ui/maindisplay.py +++ b/openlp/core/ui/maindisplay.py @@ -37,6 +37,7 @@ from PyQt5 import QtCore, QtWidgets, QtWebKit, QtWebKitWidgets, QtGui, QtMultime from openlp.core.common import AppLocation, Registry, RegistryProperties, OpenLPMixin, Settings, translate,\ is_macosx, is_win +from openlp.core.common.path import path_to_str from openlp.core.lib import ServiceItem, ImageSource, ScreenList, build_html, expand_tags, image_to_byte from openlp.core.lib.theme import BackgroundType from openlp.core.ui import HideMode, AlertLocation, DisplayControllerType @@ -259,7 +260,7 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties): background_color.setNamedColor(Settings().value('core/logo background color')) if not background_color.isValid(): background_color = QtCore.Qt.white - image_file = Settings().value('core/logo file') + image_file = path_to_str(Settings().value('core/logo file')) splash_image = QtGui.QImage(image_file) self.initial_fame = QtGui.QImage( self.screen['size'].width(), diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 54f70ccb2..413e97a17 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -30,7 +30,6 @@ import time from datetime import datetime from distutils import dir_util from distutils.errors import DistutilsFileError -from pathlib import Path from tempfile import gettempdir from PyQt5 import QtCore, QtGui, QtWidgets @@ -40,6 +39,7 @@ from openlp.core.api.http import server from openlp.core.common import Registry, RegistryProperties, AppLocation, LanguageManager, Settings, UiStrings, \ check_directory_exists, translate, is_win, is_macosx, add_actions from openlp.core.common.actions import ActionList, CategoryOrder +from openlp.core.common.path import Path, path_to_str, str_to_path from openlp.core.common.versionchecker import get_application_version from openlp.core.lib import Renderer, PluginManager, ImageManager, PluginStatus, ScreenList, build_icon from openlp.core.lib.ui import create_action @@ -50,6 +50,7 @@ from openlp.core.ui.media import MediaController from openlp.core.ui.printserviceform import PrintServiceForm from openlp.core.ui.projector.manager import ProjectorManager from openlp.core.ui.lib.dockwidget import OpenLPDockWidget +from openlp.core.ui.lib.filedialog import FileDialog from openlp.core.ui.lib.mediadockmanager import MediaDockManager @@ -876,11 +877,12 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): shutil.copyfile(import_file_name, temp_config) settings = Settings() import_settings = Settings(temp_config, Settings.IniFormat) - # Convert image files + log.info('hook upgrade_plugin_settings') self.plugin_manager.hook_upgrade_plugin_settings(import_settings) - # Remove/rename old settings to prepare the import. - import_settings.remove_obsolete_settings() + # Upgrade settings to prepare the import. + if import_settings.can_upgrade(): + import_settings.upgrade_settings() # Lets do a basic sanity check. If it contains this string we can assume it was created by OpenLP and so we'll # load what we can from it, and just silently ignore anything we don't recognise. if import_settings.value('SettingsImport/type') != 'OpenLP_settings_export': @@ -936,89 +938,17 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): """ Export settings to a .conf file in INI format """ - export_file_name, filter_used = QtWidgets.QFileDialog.getSaveFileName( + export_file_path, filter_used = FileDialog.getSaveFileName( self, translate('OpenLP.MainWindow', 'Export Settings File'), - '', + None, translate('OpenLP.MainWindow', 'OpenLP Settings (*.conf)')) - if not export_file_name: + if not export_file_path: return - # Make sure it's a .conf file. - if not export_file_name.endswith('conf'): - export_file_name += '.conf' - temp_file = os.path.join(gettempdir(), 'openlp', 'exportConf.tmp') + # Make sure it's a .conf file. + export_file_path = export_file_path.with_suffix('.conf') self.save_settings() - setting_sections = [] - # Add main sections. - setting_sections.extend([self.general_settings_section]) - setting_sections.extend([self.advanced_settings_section]) - setting_sections.extend([self.ui_settings_section]) - setting_sections.extend([self.shortcuts_settings_section]) - setting_sections.extend([self.service_manager_settings_section]) - setting_sections.extend([self.themes_settings_section]) - setting_sections.extend([self.display_tags_section]) - # Add plugin sections. - for plugin in self.plugin_manager.plugins: - setting_sections.extend([plugin.name]) - # Delete old files if found. - if os.path.exists(temp_file): - os.remove(temp_file) - if os.path.exists(export_file_name): - os.remove(export_file_name) - settings = Settings() - settings.remove(self.header_section) - # Get the settings. - keys = settings.allKeys() - export_settings = Settings(temp_file, Settings.IniFormat) - # Add a header section. - # This is to insure it's our conf file for import. - now = datetime.now() - application_version = get_application_version() - # Write INI format using Qsettings. - # Write our header. - export_settings.beginGroup(self.header_section) - export_settings.setValue('Make_Changes', 'At_Own_RISK') - export_settings.setValue('type', 'OpenLP_settings_export') - export_settings.setValue('file_date_created', now.strftime("%Y-%m-%d %H:%M")) - export_settings.setValue('version', application_version['full']) - export_settings.endGroup() - # Write all the sections and keys. - for section_key in keys: - # FIXME: We are conflicting with the standard "General" section. - if 'eneral' in section_key: - section_key = section_key.lower() - try: - key_value = settings.value(section_key) - except KeyError: - QtWidgets.QMessageBox.critical(self, translate('OpenLP.MainWindow', 'Export setting error'), - translate('OpenLP.MainWindow', 'The key "{key}" does not have a default ' - 'value so it will be skipped in this ' - 'export.').format(key=section_key), - QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Ok)) - key_value = None - if key_value is not None: - export_settings.setValue(section_key, key_value) - export_settings.sync() - # Temp CONF file has been written. Blanks in keys are now '%20'. - # Read the temp file and output the user's CONF file with blanks to - # make it more readable. - temp_conf = open(temp_file, 'r') - try: - export_conf = open(export_file_name, 'w') - for file_record in temp_conf: - # Get rid of any invalid entries. - if file_record.find('@Invalid()') == -1: - file_record = file_record.replace('%20', ' ') - export_conf.write(file_record) - temp_conf.close() - export_conf.close() - os.remove(temp_file) - except OSError as ose: - QtWidgets.QMessageBox.critical(self, translate('OpenLP.MainWindow', 'Export setting error'), - translate('OpenLP.MainWindow', - 'An error occurred while exporting the ' - 'settings: {err}').format(err=ose.strerror), - QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Ok)) + Settings().export(export_file_path) def on_mode_default_item_clicked(self): """ @@ -1277,7 +1207,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): settings.remove('custom slide') settings.remove('service') settings.beginGroup(self.general_settings_section) - self.recent_files = settings.value('recent files') + self.recent_files = [path_to_str(file_path) for file_path in settings.value('recent files')] settings.endGroup() settings.beginGroup(self.ui_settings_section) self.move(settings.value('main window position')) @@ -1301,7 +1231,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): log.debug('Saving QSettings') settings = Settings() settings.beginGroup(self.general_settings_section) - settings.setValue('recent files', self.recent_files) + settings.setValue('recent files', [str_to_path(file) for file in self.recent_files]) settings.endGroup() settings.beginGroup(self.ui_settings_section) settings.setValue('main window position', self.pos()) @@ -1443,7 +1373,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): log.info('No data copy requested') # Change the location of data directory in config file. settings = QtCore.QSettings() - settings.setValue('advanced/data path', self.new_data_path) + settings.setValue('advanced/data path', Path(self.new_data_path)) # Check if the new data path is our default. if self.new_data_path == str(AppLocation.get_directory(AppLocation.DataDir)): settings.remove('advanced/data path') diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index bb37aa5c7..5051e43c1 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -28,7 +28,6 @@ import os import shutil import zipfile from datetime import datetime, timedelta -from pathlib import Path from tempfile import mkstemp from PyQt5 import QtCore, QtGui, QtWidgets @@ -36,11 +35,13 @@ from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, ThemeLevel, OpenLPMixin, \ RegistryMixin, check_directory_exists, UiStrings, translate, split_filename, delete_file from openlp.core.common.actions import ActionList, CategoryOrder +from openlp.core.common.languagemanager import format_time +from openlp.core.common.path import Path, path_to_str, str_to_path from openlp.core.lib import ServiceItem, ItemCapabilities, PluginStatus, build_icon from openlp.core.lib.ui import critical_error_message_box, create_widget_action, find_and_set_in_combo_box from openlp.core.ui import ServiceNoteForm, ServiceItemEditForm, StartTimeForm from openlp.core.ui.lib import OpenLPToolbar -from openlp.core.common.languagemanager import format_time +from openlp.core.ui.lib.filedialog import FileDialog class ServiceManagerList(QtWidgets.QTreeWidget): @@ -373,7 +374,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa """ self._file_name = str(file_name) self.main_window.set_service_modified(self.is_modified(), self.short_file_name()) - Settings().setValue('servicemanager/last file', file_name) + Settings().setValue('servicemanager/last file', Path(file_name)) self._save_lite = self._file_name.endswith('.oszl') def file_name(self): @@ -435,18 +436,17 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa elif result == QtWidgets.QMessageBox.Save: self.decide_save_method() if not load_file: - file_name, filter_used = QtWidgets.QFileDialog.getOpenFileName( + file_path, filter_used = FileDialog.getOpenFileName( self.main_window, translate('OpenLP.ServiceManager', 'Open File'), Settings().value(self.main_window.service_manager_settings_section + '/last directory'), translate('OpenLP.ServiceManager', 'OpenLP Service Files (*.osz *.oszl)')) - if not file_name: + if not file_path: return False else: - file_name = load_file - Settings().setValue(self.main_window.service_manager_settings_section + '/last directory', - split_filename(file_name)[0]) - self.load_file(file_name) + file_path = str_to_path(load_file) + Settings().setValue(self.main_window.service_manager_settings_section + '/last directory', file_path.parent) + self.load_file(str(file_path)) def save_modified_service(self): """ @@ -477,7 +477,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa self.set_file_name('') self.service_id += 1 self.set_modified(False) - Settings().setValue('servicemanager/last file', '') + Settings().setValue('servicemanager/last file', None) self.plugin_manager.new_service_created() def create_basic_service(self): @@ -513,7 +513,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa base_name = os.path.splitext(file_name)[0] service_file_name = '{name}.osj'.format(name=base_name) self.log_debug('ServiceManager.save_file - {name}'.format(name=path_file_name)) - Settings().setValue(self.main_window.service_manager_settings_section + '/last directory', path) + Settings().setValue(self.main_window.service_manager_settings_section + '/last directory', Path(path)) service = self.create_basic_service() write_list = [] missing_list = [] @@ -634,7 +634,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa base_name = os.path.splitext(file_name)[0] service_file_name = '{name}.osj'.format(name=base_name) self.log_debug('ServiceManager.save_file - {name}'.format(name=path_file_name)) - Settings().setValue(self.main_window.service_manager_settings_section + '/last directory', path) + Settings().setValue(self.main_window.service_manager_settings_section + '/last directory', Path(path)) service = self.create_basic_service() self.application.set_busy_cursor() # Number of items + 1 to zip it @@ -695,7 +695,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa default_file_name = format_time(default_pattern, local_time) else: default_file_name = '' - directory = Settings().value(self.main_window.service_manager_settings_section + '/last directory') + directory = path_to_str(Settings().value(self.main_window.service_manager_settings_section + '/last directory')) path = os.path.join(directory, default_file_name) # SaveAs from osz to oszl is not valid as the files will be deleted on exit which is not sensible or usable in # the long term. @@ -778,7 +778,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa delete_file(Path(p_file)) self.main_window.add_recent_file(file_name) self.set_modified(False) - Settings().setValue('servicemanager/last file', file_name) + Settings().setValue('servicemanager/last file', Path(file_name)) else: critical_error_message_box(message=translate('OpenLP.ServiceManager', 'File is not a valid service.')) self.log_error('File contains no service data') @@ -843,7 +843,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa Load the last service item from the service manager when the service was last closed. Can be blank if there was no service present. """ - file_name = Settings().value('servicemanager/last file') + file_name = str_to_path(Settings().value('servicemanager/last file')) if file_name: self.load_file(file_name) diff --git a/openlp/core/ui/themeform.py b/openlp/core/ui/themeform.py index 69a45015a..cac45f28d 100644 --- a/openlp/core/ui/themeform.py +++ b/openlp/core/ui/themeform.py @@ -24,12 +24,11 @@ The Theme wizard """ import logging import os -from pathlib import Path from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import Registry, RegistryProperties, UiStrings, translate, get_images_filter, is_not_image_file -from openlp.core.common.path import path_to_str, str_to_path +from openlp.core.common.path import Path, path_to_str, str_to_path from openlp.core.lib.theme import BackgroundType, BackgroundGradientType from openlp.core.lib.ui import critical_error_message_box from openlp.core.ui import ThemeLayoutForm diff --git a/openlp/core/ui/thememanager.py b/openlp/core/ui/thememanager.py index ec702c831..61381a902 100644 --- a/openlp/core/ui/thememanager.py +++ b/openlp/core/ui/thememanager.py @@ -25,14 +25,13 @@ The Theme Manager manages adding, deleteing and modifying of themes. import os import zipfile import shutil -from pathlib import Path from xml.etree.ElementTree import ElementTree, XML from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, OpenLPMixin, RegistryMixin, \ UiStrings, check_directory_exists, translate, is_win, get_filesystem_encoding, delete_file -from openlp.core.common.path import path_to_str, str_to_path +from openlp.core.common.path import Path, path_to_str, str_to_path from openlp.core.lib import ImageSource, ValidationError, get_text_file_string, build_icon, \ check_item_selected, create_thumb, validate_thumb from openlp.core.lib.theme import Theme, BackgroundType @@ -379,16 +378,16 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage critical_error_message_box(message=translate('OpenLP.ThemeManager', 'You have not selected a theme.')) return theme = item.data(QtCore.Qt.UserRole) - path, filter_used = \ - QtWidgets.QFileDialog.getSaveFileName(self.main_window, - translate('OpenLP.ThemeManager', 'Save Theme - ({name})'). - format(name=theme), - Settings().value(self.settings_section + '/last directory export'), - translate('OpenLP.ThemeManager', 'OpenLP Themes (*.otz)')) + export_path, filter_used = \ + FileDialog.getSaveFileName(self.main_window, + translate('OpenLP.ThemeManager', 'Save Theme - ({name})'). + format(name=theme), + Settings().value(self.settings_section + '/last directory export'), + translate('OpenLP.ThemeManager', 'OpenLP Themes (*.otz)')) self.application.set_busy_cursor() - if path: - Settings().setValue(self.settings_section + '/last directory export', path) - if self._export_theme(path, theme): + if export_path: + Settings().setValue(self.settings_section + '/last directory export', export_path.parent) + if self._export_theme(str(export_path), theme): QtWidgets.QMessageBox.information(self, translate('OpenLP.ThemeManager', 'Theme Exported'), translate('OpenLP.ThemeManager', @@ -429,16 +428,15 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage file_paths, selected_filter = FileDialog.getOpenFileNames( self, translate('OpenLP.ThemeManager', 'Select Theme Import File'), - str_to_path(Settings().value(self.settings_section + '/last directory import')), + Settings().value(self.settings_section + '/last directory import'), translate('OpenLP.ThemeManager', 'OpenLP Themes (*.otz)')) self.log_info('New Themes {file_paths}'.format(file_paths=file_paths)) if not file_paths: return self.application.set_busy_cursor() for file_path in file_paths: - file_name = path_to_str(file_path) - Settings().setValue(self.settings_section + '/last directory import', str(file_name)) - self.unzip_theme(file_name, self.path) + self.unzip_theme(path_to_str(file_path), self.path) + Settings().setValue(self.settings_section + '/last directory import', file_path) self.load_themes() self.application.set_normal_cursor() diff --git a/openlp/core/ui/themewizard.py b/openlp/core/ui/themewizard.py index cd1748aae..80efa5fdf 100644 --- a/openlp/core/ui/themewizard.py +++ b/openlp/core/ui/themewizard.py @@ -22,11 +22,10 @@ """ The Create/Edit theme wizard """ -from pathlib import Path - from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import UiStrings, translate, is_macosx +from openlp.core.common.path import Path from openlp.core.lib import build_icon from openlp.core.lib.theme import HorizontalType, BackgroundType, BackgroundGradientType from openlp.core.lib.ui import add_welcome_page, create_valign_selection_widgets diff --git a/openlp/plugins/bibles/bibleplugin.py b/openlp/plugins/bibles/bibleplugin.py index bcc81f9cb..4829ff234 100644 --- a/openlp/plugins/bibles/bibleplugin.py +++ b/openlp/plugins/bibles/bibleplugin.py @@ -59,7 +59,7 @@ __default_settings__ = { 'bibles/range separator': '', 'bibles/list separator': '', 'bibles/end separator': '', - 'bibles/last directory import': '', + 'bibles/last directory import': None, 'bibles/hide combined quick error': False, 'bibles/is search while typing enabled': True } diff --git a/openlp/plugins/bibles/lib/importers/csvbible.py b/openlp/plugins/bibles/lib/importers/csvbible.py index cac6571a9..e319bdf36 100644 --- a/openlp/plugins/bibles/lib/importers/csvbible.py +++ b/openlp/plugins/bibles/lib/importers/csvbible.py @@ -51,9 +51,9 @@ All CSV files are expected to use a comma (',') as the delimiter and double quot """ import csv from collections import namedtuple -from pathlib import Path from openlp.core.common import get_file_encoding, translate +from openlp.core.common.path import Path from openlp.core.lib.exceptions import ValidationError from openlp.plugins.bibles.lib.bibleimport import BibleImport diff --git a/openlp/plugins/bibles/lib/manager.py b/openlp/plugins/bibles/lib/manager.py index a6de936ea..5c8728e58 100644 --- a/openlp/plugins/bibles/lib/manager.py +++ b/openlp/plugins/bibles/lib/manager.py @@ -21,10 +21,9 @@ ############################################################################### import logging -import os -from pathlib import Path from openlp.core.common import AppLocation, OpenLPMixin, RegistryProperties, Settings, translate, delete_file, UiStrings +from openlp.core.common.path import Path from openlp.plugins.bibles.lib import LanguageSelection, parse_reference from openlp.plugins.bibles.lib.db import BibleDB, BibleMeta from .importers.csvbible import CSVBible @@ -306,13 +305,10 @@ class BibleManager(OpenLPMixin, RegistryProperties): """ Does a verse search for the given bible and text. - :param bible: The bible to search - :type bible: str - :param text: The text to search for - :type text: str - + :param str bible: The bible to search + :param str text: The text to search for :return: The search results if valid, or None if the search is invalid. - :rtype: None, list + :rtype: None | list """ log.debug('BibleManager.verse_search("{bible}", "{text}")'.format(bible=bible, text=text)) if not text: diff --git a/openlp/plugins/bibles/lib/mediaitem.py b/openlp/plugins/bibles/lib/mediaitem.py index 1c210c9ed..a05747705 100755 --- a/openlp/plugins/bibles/lib/mediaitem.py +++ b/openlp/plugins/bibles/lib/mediaitem.py @@ -465,8 +465,7 @@ class BibleMediaItem(MediaManagerItem): """ Show the selected tab and set focus to it - :param index: The tab selected - :type index: int + :param int index: The tab selected :return: None """ if index == SearchTabs.Search or index == SearchTabs.Select: @@ -483,7 +482,7 @@ class BibleMediaItem(MediaManagerItem): Update list_widget with the contents of the selected list :param index: The index of the tab that has been changed to. (int) - :return: None + :rtype: None """ if index == ResultsTab.Saved: self.add_built_results_to_list_widget(self.saved_results) diff --git a/openlp/plugins/images/imageplugin.py b/openlp/plugins/images/imageplugin.py index 75d6ceab3..c4da26dfe 100644 --- a/openlp/plugins/images/imageplugin.py +++ b/openlp/plugins/images/imageplugin.py @@ -71,14 +71,6 @@ class ImagePlugin(Plugin): 'provided by the theme.') return about_text - def upgrade_settings(self, settings): - """ - Upgrade the settings of this plugin. - - :param settings: The Settings object containing the old settings. - """ - pass - def set_plugin_text_strings(self): """ Called to define all translatable texts of the plugin. diff --git a/openlp/plugins/images/lib/mediaitem.py b/openlp/plugins/images/lib/mediaitem.py index 994a45b93..bcf222eb0 100644 --- a/openlp/plugins/images/lib/mediaitem.py +++ b/openlp/plugins/images/lib/mediaitem.py @@ -22,12 +22,12 @@ import logging import os -from pathlib import Path from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import Registry, AppLocation, Settings, UiStrings, check_directory_exists, translate, \ delete_file, get_images_filter +from openlp.core.common.path import Path from openlp.core.lib import ItemCapabilities, MediaManagerItem, ServiceItemContext, StringContent, build_icon, \ check_item_selected, create_thumb, validate_thumb from openlp.core.lib.ui import create_widget_action, critical_error_message_box @@ -390,7 +390,7 @@ class ImageMediaItem(MediaManagerItem): self.application.set_normal_cursor() self.load_list(files, target_group) last_dir = os.path.split(files[0])[0] - Settings().setValue(self.settings_section + '/last directory', last_dir) + Settings().setValue(self.settings_section + '/last directory', Path(last_dir)) def load_list(self, images, target_group=None, initial_load=False): """ diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index ea2142c1d..154d033c1 100644 --- a/openlp/plugins/media/lib/mediaitem.py +++ b/openlp/plugins/media/lib/mediaitem.py @@ -22,12 +22,12 @@ import logging import os -from pathlib import Path from PyQt5 import QtCore, QtWidgets from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, check_directory_exists, UiStrings,\ translate +from openlp.core.common.path import Path, path_to_str, str_to_path from openlp.core.lib import ItemCapabilities, MediaManagerItem, MediaType, ServiceItem, ServiceItemContext, \ build_icon, check_item_selected from openlp.core.lib.ui import create_widget_action, critical_error_message_box, create_horizontal_adjusting_combo_box @@ -303,7 +303,7 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): self.list_view.clear() self.service_path = os.path.join(str(AppLocation.get_section_data_path(self.settings_section)), 'thumbnails') check_directory_exists(Path(self.service_path)) - self.load_list(Settings().value(self.settings_section + '/media files')) + self.load_list([path_to_str(file) for file in Settings().value(self.settings_section + '/media files')]) self.rebuild_players() def rebuild_players(self): @@ -401,14 +401,14 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): :param media_type: Type to get, defaults to audio. :return: The media list """ - media = Settings().value(self.settings_section + '/media files') - media.sort(key=lambda filename: get_locale_key(os.path.split(str(filename))[1])) + media_file_paths = Settings().value(self.settings_section + '/media files') + media_file_paths.sort(key=lambda file_path: get_locale_key(file_path.name)) if media_type == MediaType.Audio: extension = self.media_controller.audio_extensions_list else: extension = self.media_controller.video_extensions_list extension = [x[1:] for x in extension] - media = [x for x in media if os.path.splitext(x)[1] in extension] + media = [x for x in media_file_paths if x.suffix in extension] return media def search(self, string, show_error): @@ -419,13 +419,12 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): :param show_error: Should the error be shown (True) :return: The search result. """ - files = Settings().value(self.settings_section + '/media files') results = [] string = string.lower() - for file in files: - filename = os.path.split(str(file))[1] - if filename.lower().find(string) > -1: - results.append([file, filename]) + for file_path in Settings().value(self.settings_section + '/media files'): + file_name = file_path.name + if file_name.lower().find(string) > -1: + results.append([str(file_path), file_name]) return results def on_load_optical(self): @@ -446,13 +445,13 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): :param optical: The clip to add. """ - full_list = self.get_file_list() + file_paths = self.get_file_list() # If the clip already is in the media list it isn't added and an error message is displayed. - if optical in full_list: + if optical in file_paths: critical_error_message_box(translate('MediaPlugin.MediaItem', 'Mediaclip already saved'), translate('MediaPlugin.MediaItem', 'This mediaclip has already been saved')) return # Append the optical string to the media list - full_list.append(optical) + file_paths.append(optical) self.load_list([optical]) - Settings().setValue(self.settings_section + '/media files', self.get_file_list()) + Settings().setValue(self.settings_section + '/media files', file_paths) diff --git a/openlp/plugins/media/mediaplugin.py b/openlp/plugins/media/mediaplugin.py index af22a4f65..1c6806b2c 100644 --- a/openlp/plugins/media/mediaplugin.py +++ b/openlp/plugins/media/mediaplugin.py @@ -26,12 +26,11 @@ The Media plugin import logging import os import re -from pathlib import Path - from PyQt5 import QtCore from openlp.core.api.http import register_endpoint from openlp.core.common import AppLocation, translate, check_binary_exists +from openlp.core.common.path import Path from openlp.core.lib import Plugin, StringContent, build_icon from openlp.plugins.media.endpoint import api_media_endpoint, media_endpoint from openlp.plugins.media.lib import MediaMediaItem, MediaTab diff --git a/openlp/plugins/presentations/lib/impresscontroller.py b/openlp/plugins/presentations/lib/impresscontroller.py index fc9dc84d1..25c470f48 100644 --- a/openlp/plugins/presentations/lib/impresscontroller.py +++ b/openlp/plugins/presentations/lib/impresscontroller.py @@ -34,9 +34,9 @@ import logging import os import time -from pathlib import Path -from openlp.core.common import is_win, Registry, get_uno_command, get_uno_instance, delete_file +from openlp.core.common import is_win, Registry, delete_file +from openlp.core.common.path import Path if is_win(): from win32com.client import Dispatch diff --git a/openlp/plugins/presentations/lib/mediaitem.py b/openlp/plugins/presentations/lib/mediaitem.py index 9e2921667..99c937eb0 100644 --- a/openlp/plugins/presentations/lib/mediaitem.py +++ b/openlp/plugins/presentations/lib/mediaitem.py @@ -26,10 +26,11 @@ import os from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import Registry, Settings, UiStrings, translate +from openlp.core.common.languagemanager import get_locale_key +from openlp.core.common.path import path_to_str from openlp.core.lib import MediaManagerItem, ItemCapabilities, ServiceItemContext,\ build_icon, check_item_selected, create_thumb, validate_thumb from openlp.core.lib.ui import critical_error_message_box, create_horizontal_adjusting_combo_box -from openlp.core.common.languagemanager import get_locale_key from openlp.plugins.presentations.lib import MessageListener from openlp.plugins.presentations.lib.pdfcontroller import PDF_CONTROLLER_FILETYPES @@ -126,8 +127,8 @@ class PresentationMediaItem(MediaManagerItem): Populate the media manager tab """ self.list_view.setIconSize(QtCore.QSize(88, 50)) - files = Settings().value(self.settings_section + '/presentations files') - self.load_list(files, initial_load=True) + file_paths = Settings().value(self.settings_section + '/presentations files') + self.load_list([path_to_str(file) for file in file_paths], initial_load=True) self.populate_display_types() def populate_display_types(self): @@ -157,7 +158,7 @@ class PresentationMediaItem(MediaManagerItem): existing files, and when the user adds new files via the media manager. """ current_list = self.get_file_list() - titles = [os.path.split(file)[1] for file in current_list] + titles = [file_path.name for file_path in current_list] self.application.set_busy_cursor() if not initial_load: self.main_window.display_progress_bar(len(files)) @@ -410,11 +411,11 @@ class PresentationMediaItem(MediaManagerItem): :param show_error: not used :return: """ - files = Settings().value(self.settings_section + '/presentations files') + file_paths = Settings().value(self.settings_section + '/presentations files') results = [] string = string.lower() - for file in files: - filename = os.path.split(str(file))[1] - if filename.lower().find(string) > -1: - results.append([file, filename]) + for file_path in file_paths: + file_name = file_path.name + if file_name.lower().find(string) > -1: + results.append([path_to_str(file_path), file_name]) return results diff --git a/openlp/plugins/presentations/lib/pdfcontroller.py b/openlp/plugins/presentations/lib/pdfcontroller.py index b7b6b2a12..f4a091551 100644 --- a/openlp/plugins/presentations/lib/pdfcontroller.py +++ b/openlp/plugins/presentations/lib/pdfcontroller.py @@ -23,12 +23,12 @@ import os import logging import re -from pathlib import Path from shutil import which from subprocess import check_output, CalledProcessError from openlp.core.common import AppLocation, check_binary_exists from openlp.core.common import Settings, is_win +from openlp.core.common.path import Path, path_to_str from openlp.core.lib import ScreenList from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument @@ -113,7 +113,7 @@ class PdfController(PresentationController): self.also_supports = [] # Use the user defined program if given if Settings().value('presentations/enable_pdf_program'): - pdf_program = Settings().value('presentations/pdf_program') + pdf_program = path_to_str(Settings().value('presentations/pdf_program')) program_type = self.process_check_binary(pdf_program) if program_type == 'gs': self.gsbin = pdf_program diff --git a/openlp/plugins/presentations/lib/presentationcontroller.py b/openlp/plugins/presentations/lib/presentationcontroller.py index d0d2063d1..e5f8ccf21 100644 --- a/openlp/plugins/presentations/lib/presentationcontroller.py +++ b/openlp/plugins/presentations/lib/presentationcontroller.py @@ -23,11 +23,11 @@ import logging import os import shutil -from pathlib import Path from PyQt5 import QtCore from openlp.core.common import Registry, AppLocation, Settings, check_directory_exists, md5_hash +from openlp.core.common.path import Path from openlp.core.lib import create_thumb, validate_thumb log = logging.getLogger(__name__) diff --git a/openlp/plugins/presentations/lib/presentationtab.py b/openlp/plugins/presentations/lib/presentationtab.py index fa804f2d3..3e92827c8 100644 --- a/openlp/plugins/presentations/lib/presentationtab.py +++ b/openlp/plugins/presentations/lib/presentationtab.py @@ -155,9 +155,7 @@ class PresentationTab(SettingsTab): enable_pdf_program = Settings().value(self.settings_section + '/enable_pdf_program') self.pdf_program_check_box.setChecked(enable_pdf_program) self.program_path_edit.setEnabled(enable_pdf_program) - pdf_program = Settings().value(self.settings_section + '/pdf_program') - if pdf_program: - self.program_path_edit.path = str_to_path(pdf_program) + self.program_path_edit.path = Settings().value(self.settings_section + '/pdf_program') def save(self): """ @@ -193,13 +191,13 @@ class PresentationTab(SettingsTab): Settings().setValue(setting_key, self.ppt_window_check_box.checkState()) changed = True # Save pdf-settings - pdf_program = path_to_str(self.program_path_edit.path) + pdf_program_path = self.program_path_edit.path enable_pdf_program = self.pdf_program_check_box.checkState() # If the given program is blank disable using the program - if pdf_program == '': + if not pdf_program_path: enable_pdf_program = 0 - if pdf_program != Settings().value(self.settings_section + '/pdf_program'): - Settings().setValue(self.settings_section + '/pdf_program', pdf_program) + if pdf_program_path != Settings().value(self.settings_section + '/pdf_program'): + Settings().setValue(self.settings_section + '/pdf_program', pdf_program_path) changed = True if enable_pdf_program != Settings().value(self.settings_section + '/enable_pdf_program'): Settings().setValue(self.settings_section + '/enable_pdf_program', enable_pdf_program) diff --git a/openlp/plugins/presentations/presentationplugin.py b/openlp/plugins/presentations/presentationplugin.py index a91f78794..6f9989565 100644 --- a/openlp/plugins/presentations/presentationplugin.py +++ b/openlp/plugins/presentations/presentationplugin.py @@ -39,7 +39,7 @@ log = logging.getLogger(__name__) __default_settings__ = {'presentations/override app': QtCore.Qt.Unchecked, 'presentations/enable_pdf_program': QtCore.Qt.Unchecked, - 'presentations/pdf_program': '', + 'presentations/pdf_program': None, 'presentations/Impress': QtCore.Qt.Checked, 'presentations/Powerpoint': QtCore.Qt.Checked, 'presentations/Powerpoint Viewer': QtCore.Qt.Checked, diff --git a/openlp/plugins/songs/forms/editsongform.py b/openlp/plugins/songs/forms/editsongform.py index aab09b9a8..a8c0ed32f 100644 --- a/openlp/plugins/songs/forms/editsongform.py +++ b/openlp/plugins/songs/forms/editsongform.py @@ -28,12 +28,11 @@ import logging import re import os import shutil -from pathlib import Path from PyQt5 import QtCore, QtWidgets from openlp.core.common import Registry, RegistryProperties, AppLocation, UiStrings, check_directory_exists, translate -from openlp.core.common.path import path_to_str +from openlp.core.common.path import Path, path_to_str from openlp.core.lib import PluginStatus, MediaType, create_separated_list from openlp.core.lib.ui import set_case_insensitive_completer, critical_error_message_box, find_and_set_in_combo_box from openlp.core.ui.lib.filedialog import FileDialog diff --git a/openlp/plugins/songs/forms/songimportform.py b/openlp/plugins/songs/forms/songimportform.py index e3c0eb620..502d40126 100644 --- a/openlp/plugins/songs/forms/songimportform.py +++ b/openlp/plugins/songs/forms/songimportform.py @@ -239,13 +239,11 @@ class SongImportForm(OpenLPWizard, RegistryProperties): filters += ';;' filters += '{text} (*)'.format(text=UiStrings().AllFiles) file_paths, selected_filter = FileDialog.getOpenFileNames( - self, title, - str_to_path(Settings().value(self.plugin.settings_section + '/last directory import')), filters) + self, title, Settings().value(self.plugin.settings_section + '/last directory import'), filters) if file_paths: file_names = [path_to_str(file_path) for file_path in file_paths] listbox.addItems(file_names) - Settings().setValue(self.plugin.settings_section + '/last directory import', - os.path.split(str(file_names[0]))[0]) + Settings().setValue(self.plugin.settings_section + '/last directory import', file_paths[0].parent) def get_list_of_files(self, list_box): """ @@ -363,14 +361,15 @@ class SongImportForm(OpenLPWizard, RegistryProperties): def on_error_save_to_button_clicked(self): """ Save the error report to a file. + + :rtype: None """ - filename, filter_used = QtWidgets.QFileDialog.getSaveFileName( + file_path, filter_used = FileDialog.getSaveFileName( self, Settings().value(self.plugin.settings_section + '/last directory import')) - if not filename: + if not file_path: return - report_file = codecs.open(filename, 'w', 'utf-8') - report_file.write(self.error_report_text_edit.toPlainText()) - report_file.close() + with file_path.open('w', encoding='utf-8') as report_file: + report_file.write(self.error_report_text_edit.toPlainText()) def add_file_select_item(self): """ diff --git a/openlp/plugins/songs/lib/importers/songbeamer.py b/openlp/plugins/songs/lib/importers/songbeamer.py index 70edf1ad8..ff8b1f7a6 100644 --- a/openlp/plugins/songs/lib/importers/songbeamer.py +++ b/openlp/plugins/songs/lib/importers/songbeamer.py @@ -27,11 +27,11 @@ import os import re import base64 import math -from pathlib import Path +from openlp.core.common import Settings, is_win, is_macosx, get_file_encoding +from openlp.core.common.path import Path from openlp.plugins.songs.lib import VerseType from openlp.plugins.songs.lib.importers.songimport import SongImport -from openlp.core.common import Settings, is_win, is_macosx, get_file_encoding log = logging.getLogger(__name__) diff --git a/openlp/plugins/songs/lib/importers/songimport.py b/openlp/plugins/songs/lib/importers/songimport.py index 8265cfc80..ff190f0e0 100644 --- a/openlp/plugins/songs/lib/importers/songimport.py +++ b/openlp/plugins/songs/lib/importers/songimport.py @@ -24,11 +24,11 @@ import logging import re import shutil import os -from pathlib import Path from PyQt5 import QtCore from openlp.core.common import Registry, AppLocation, check_directory_exists, translate +from openlp.core.common.path import Path from openlp.core.ui.lib.wizard import WizardStrings from openlp.plugins.songs.lib import clean_song, VerseType from openlp.plugins.songs.lib.db import Song, Author, Topic, Book, MediaFile diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 65d196f93..31bd18b62 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -23,12 +23,12 @@ import logging import os import shutil -from pathlib import Path from PyQt5 import QtCore, QtWidgets from sqlalchemy.sql import and_, or_ from openlp.core.common import Registry, AppLocation, Settings, check_directory_exists, UiStrings, translate +from openlp.core.common.path import Path from openlp.core.lib import MediaManagerItem, ItemCapabilities, PluginStatus, ServiceItemContext, \ check_item_selected, create_separated_list from openlp.core.lib.ui import create_widget_action diff --git a/openlp/plugins/songs/lib/openlyricsexport.py b/openlp/plugins/songs/lib/openlyricsexport.py index 9c2c923b1..04677396b 100644 --- a/openlp/plugins/songs/lib/openlyricsexport.py +++ b/openlp/plugins/songs/lib/openlyricsexport.py @@ -25,11 +25,11 @@ format. """ import logging import os -from pathlib import Path from lxml import etree from openlp.core.common import RegistryProperties, check_directory_exists, translate, clean_filename +from openlp.core.common.path import Path from openlp.plugins.songs.lib.openlyricsxml import OpenLyrics log = logging.getLogger(__name__) diff --git a/openlp/plugins/songs/songsplugin.py b/openlp/plugins/songs/songsplugin.py index f156f3ee3..1edb999dc 100644 --- a/openlp/plugins/songs/songsplugin.py +++ b/openlp/plugins/songs/songsplugin.py @@ -66,8 +66,8 @@ __default_settings__ = { 'songs/display songbook': False, 'songs/display written by': True, 'songs/display copyright symbol': False, - 'songs/last directory import': '', - 'songs/last directory export': '', + 'songs/last directory import': None, + 'songs/last directory export': None, 'songs/songselect username': '', 'songs/songselect password': '', 'songs/songselect searches': '', diff --git a/openlp/plugins/songusage/forms/songusagedetailform.py b/openlp/plugins/songusage/forms/songusagedetailform.py index 9697fdbb3..7aa636635 100644 --- a/openlp/plugins/songusage/forms/songusagedetailform.py +++ b/openlp/plugins/songusage/forms/songusagedetailform.py @@ -22,13 +22,12 @@ import logging import os -from pathlib import Path from PyQt5 import QtCore, QtWidgets from sqlalchemy.sql import and_ from openlp.core.common import RegistryProperties, Settings, check_directory_exists, translate -from openlp.core.common.path import path_to_str, str_to_path +from openlp.core.common.path import Path, path_to_str, str_to_path from openlp.core.lib.ui import critical_error_message_box from openlp.plugins.songusage.lib.db import SongUsageItem from .songusagedetaildialog import Ui_SongUsageDetailDialog @@ -57,14 +56,16 @@ class SongUsageDetailForm(QtWidgets.QDialog, Ui_SongUsageDetailDialog, RegistryP """ self.from_date_calendar.setSelectedDate(Settings().value(self.plugin.settings_section + '/from date')) self.to_date_calendar.setSelectedDate(Settings().value(self.plugin.settings_section + '/to date')) - self.report_path_edit.path = str_to_path( - Settings().value(self.plugin.settings_section + '/last directory export')) + self.report_path_edit.path = Settings().value(self.plugin.settings_section + '/last directory export') def on_report_path_edit_path_changed(self, file_path): """ - Triggered when the Directory selection button is clicked + Called when the path in the `PathEdit` has changed + + :param openlp.core.common.path.Path file_path: The new path. + :rtype: None """ - Settings().setValue(self.plugin.settings_section + '/last directory export', path_to_str(file_path)) + Settings().setValue(self.plugin.settings_section + '/last directory export', file_path) def accept(self): """ diff --git a/openlp/plugins/songusage/songusageplugin.py b/openlp/plugins/songusage/songusageplugin.py index 705d86f5d..d0c2f7fe7 100644 --- a/openlp/plugins/songusage/songusageplugin.py +++ b/openlp/plugins/songusage/songusageplugin.py @@ -50,7 +50,7 @@ __default_settings__ = { 'songusage/active': False, 'songusage/to date': QtCore.QDate(YEAR, 8, 31), 'songusage/from date': QtCore.QDate(YEAR - 1, 9, 1), - 'songusage/last directory export': '' + 'songusage/last directory export': None } diff --git a/tests/functional/openlp_core_common/test_applocation.py b/tests/functional/openlp_core_common/test_applocation.py index e6c3b5047..7dff1a073 100644 --- a/tests/functional/openlp_core_common/test_applocation.py +++ b/tests/functional/openlp_core_common/test_applocation.py @@ -22,13 +22,12 @@ """ Functional tests to test the AppLocation class and related methods. """ -import copy import os -from pathlib import Path from unittest import TestCase from unittest.mock import MagicMock, patch from openlp.core.common import AppLocation, get_frozen_path +from openlp.core.common.path import Path FILE_LIST = ['file1', 'file2', 'file3.txt', 'file4.txt', 'file5.mp3', 'file6.mp3'] @@ -43,12 +42,14 @@ class TestAppLocation(TestCase): """ with patch('openlp.core.common.applocation.Settings') as mocked_class, \ patch('openlp.core.common.AppLocation.get_directory') as mocked_get_directory, \ - patch('openlp.core.common.applocation.check_directory_exists') as mocked_check_directory_exists: + patch('openlp.core.common.applocation.check_directory_exists') as mocked_check_directory_exists, \ + patch('openlp.core.common.applocation.os') as mocked_os: # GIVEN: A mocked out Settings class and a mocked out AppLocation.get_directory() mocked_settings = mocked_class.return_value mocked_settings.contains.return_value = False - mocked_get_directory.return_value = Path('test', 'dir') + mocked_get_directory.return_value = os.path.join('test', 'dir') mocked_check_directory_exists.return_value = True + mocked_os.path.normpath.return_value = os.path.join('test', 'dir') # WHEN: we call AppLocation.get_data_path() data_path = AppLocation.get_data_path() @@ -56,8 +57,8 @@ class TestAppLocation(TestCase): # THEN: check that all the correct methods were called, and the result is correct mocked_settings.contains.assert_called_with('advanced/data path') mocked_get_directory.assert_called_with(AppLocation.DataDir) - mocked_check_directory_exists.assert_called_with(Path('test', 'dir')) - self.assertEqual(Path('test', 'dir'), data_path, 'Result should be "test/dir"') + mocked_check_directory_exists.assert_called_with(os.path.join('test', 'dir')) + self.assertEqual(os.path.join('test', 'dir'), data_path, 'Result should be "test/dir"') def test_get_data_path_with_custom_location(self): """ diff --git a/tests/functional/openlp_core_common/test_common.py b/tests/functional/openlp_core_common/test_common.py index a09c8388d..e279cc83a 100644 --- a/tests/functional/openlp_core_common/test_common.py +++ b/tests/functional/openlp_core_common/test_common.py @@ -22,13 +22,12 @@ """ Functional tests to test the AppLocation class and related methods. """ -from pathlib import Path from unittest import TestCase from unittest.mock import MagicMock, call, patch -from openlp.core import common from openlp.core.common import check_directory_exists, clean_button_text, de_hump, extension_loader, is_macosx, \ is_linux, is_win, path_to_module, trace_error_handler, translate +from openlp.core.common.path import Path class TestCommonFunctions(TestCase): diff --git a/tests/functional/openlp_core_common/test_init.py b/tests/functional/openlp_core_common/test_init.py index 141ebdf94..532f11bac 100644 --- a/tests/functional/openlp_core_common/test_init.py +++ b/tests/functional/openlp_core_common/test_init.py @@ -24,12 +24,12 @@ Functional tests to test the AppLocation class and related methods. """ import os from io import BytesIO -from pathlib import Path from unittest import TestCase from unittest.mock import MagicMock, PropertyMock, call, patch from openlp.core.common import add_actions, clean_filename, delete_file, get_file_encoding, get_filesystem_encoding, \ get_uno_command, get_uno_instance, split_filename +from openlp.core.common.path import Path from tests.helpers.testmixin import TestMixin diff --git a/tests/functional/openlp_core_common/test_json.py b/tests/functional/openlp_core_common/test_json.py new file mode 100644 index 000000000..3b0631dc4 --- /dev/null +++ b/tests/functional/openlp_core_common/test_json.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2017 OpenLP Developers # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +Package to test the openlp.core.common.json package. +""" +import json +from unittest import TestCase +from unittest.mock import patch + +from openlp.core.common.path import Path +from openlp.core.common.json import OpenLPJsonDecoder, OpenLPJsonEncoder + + +class TestOpenLPJsonDecoder(TestCase): + """ + Test the OpenLPJsonDecoder class + """ + def test_object_hook_path_object(self): + """ + Test the object_hook method when called with a decoded Path JSON object + """ + # GIVEN: An instance of OpenLPJsonDecoder + instance = OpenLPJsonDecoder() + + # WHEN: Calling the object_hook method with a decoded JSON object which contains a Path + result = instance.object_hook({'__Path__': ['test', 'path']}) + + # THEN: A Path object should be returned + self.assertEqual(result, Path('test', 'path')) + + def test_object_hook_non_path_object(self): + """ + Test the object_hook method when called with a decoded JSON object + """ + # GIVEN: An instance of OpenLPJsonDecoder + instance = OpenLPJsonDecoder() + + # WHEN: Calling the object_hook method with a decoded JSON object which contains a Path + with patch('openlp.core.common.json.Path') as mocked_path: + result = instance.object_hook({'key': 'value'}) + + # THEN: The object should be returned unchanged and a Path object should not have been initiated + self.assertEqual(result, {'key': 'value'}) + self.assertFalse(mocked_path.called) + + def test_json_decode(self): + """ + Test the OpenLPJsonDecoder when decoding a JSON string + """ + # GIVEN: A JSON encoded string + json_string = '[{"__Path__": ["test", "path1"]}, {"__Path__": ["test", "path2"]}]' + + # WHEN: Decoding the string using the OpenLPJsonDecoder class + obj = json.loads(json_string, cls=OpenLPJsonDecoder) + + # THEN: The object returned should be a python version of the JSON string + self.assertEqual(obj, [Path('test', 'path1'), Path('test', 'path2')]) + + +class TestOpenLPJsonEncoder(TestCase): + """ + Test the OpenLPJsonEncoder class + """ + def test_default_path_object(self): + """ + Test the default method when called with a Path object + """ + # GIVEN: An instance of OpenLPJsonEncoder + instance = OpenLPJsonEncoder() + + # WHEN: Calling the default method with a Path object + result = instance.default(Path('test', 'path')) + + # THEN: A dictionary object that can be JSON encoded should be returned + self.assertEqual(result, {'__Path__': ('test', 'path')}) + + def test_default_non_path_object(self): + """ + Test the default method when called with a object other than a Path object + """ + with patch('openlp.core.common.json.JSONEncoder.default') as mocked_super_default: + + # GIVEN: An instance of OpenLPJsonEncoder + instance = OpenLPJsonEncoder() + + # WHEN: Calling the default method with a object other than a Path object + instance.default('invalid object') + + # THEN: default method of the super class should have been called + mocked_super_default.assert_called_once_with('invalid object') + + def test_json_encode(self): + """ + Test the OpenLPJsonDEncoder when encoding an object conatining Path objects + """ + # GIVEN: A list of Path objects + obj = [Path('test', 'path1'), Path('test', 'path2')] + + # WHEN: Encoding the object using the OpenLPJsonEncoder class + json_string = json.dumps(obj, cls=OpenLPJsonEncoder) + + # THEN: The JSON string return should be a representation of the object encoded + self.assertEqual(json_string, '[{"__Path__": ["test", "path1"]}, {"__Path__": ["test", "path2"]}]') diff --git a/tests/functional/openlp_core_common/test_path.py b/tests/functional/openlp_core_common/test_path.py index 2d9490012..f5abcffd5 100644 --- a/tests/functional/openlp_core_common/test_path.py +++ b/tests/functional/openlp_core_common/test_path.py @@ -23,10 +23,9 @@ Package to test the openlp.core.common.path package. """ import os -from pathlib import Path from unittest import TestCase -from openlp.core.common.path import path_to_str, str_to_path +from openlp.core.common.path import Path, path_to_str, str_to_path class TestPath(TestCase): @@ -86,3 +85,54 @@ class TestPath(TestCase): # THEN: `path_to_str` should return None self.assertEqual(result, None) + + def test_path_encode_json(self): + """ + Test that `Path.encode_json` returns a Path object from a dictionary representation of a Path object decoded + from JSON + """ + # GIVEN: A Path object from openlp.core.common.path + # WHEN: Calling encode_json, with a dictionary representation + path = Path.encode_json({'__Path__': ['path', 'to', 'fi.le']}, extra=1, args=2) + + # THEN: A Path object should have been returned + self.assertEqual(path, Path('path', 'to', 'fi.le')) + + def test_path_encode_json_base_path(self): + """ + Test that `Path.encode_json` returns a Path object from a dictionary representation of a Path object decoded + from JSON when the base_path arg is supplied. + """ + # GIVEN: A Path object from openlp.core.common.path + # WHEN: Calling encode_json, with a dictionary representation + path = Path.encode_json({'__Path__': ['path', 'to', 'fi.le']}, base_path=Path('/base')) + + # THEN: A Path object should have been returned with an absolute path + self.assertEqual(path, Path('/', 'base', 'path', 'to', 'fi.le')) + + def test_path_json_object(self): + """ + Test that `Path.json_object` creates a JSON decode-able object from a Path object + """ + # GIVEN: A Path object from openlp.core.common.path + path = Path('/base', 'path', 'to', 'fi.le') + + # WHEN: Calling json_object + obj = path.json_object(extra=1, args=2) + + # THEN: A JSON decodable object should have been returned. + self.assertEqual(obj, {'__Path__': ('/', 'base', 'path', 'to', 'fi.le')}) + + def test_path_json_object_base_path(self): + """ + Test that `Path.json_object` creates a JSON decode-able object from a Path object, that is relative to the + base_path + """ + # GIVEN: A Path object from openlp.core.common.path + path = Path('/base', 'path', 'to', 'fi.le') + + # WHEN: Calling json_object with a base_path + obj = path.json_object(base_path=Path('/', 'base')) + + # THEN: A JSON decodable object should have been returned. + self.assertEqual(obj, {'__Path__': ('path', 'to', 'fi.le')}) diff --git a/tests/functional/openlp_core_common/test_settings.py b/tests/functional/openlp_core_common/test_settings.py index 6b30d0218..fe6e68604 100644 --- a/tests/functional/openlp_core_common/test_settings.py +++ b/tests/functional/openlp_core_common/test_settings.py @@ -26,7 +26,6 @@ from unittest import TestCase from unittest.mock import patch from openlp.core.common import Settings -from openlp.core.common.settings import recent_files_conv from tests.helpers.testmixin import TestMixin @@ -48,25 +47,6 @@ class TestSettings(TestCase, TestMixin): """ self.destroy_settings() - def test_recent_files_conv(self): - """ - Test that recent_files_conv, converts various possible types of values correctly. - """ - # GIVEN: A list of possible value types and the expected results - possible_values = [(['multiple', 'values'], ['multiple', 'values']), - (['single value'], ['single value']), - ('string value', ['string value']), - (b'bytes value', ['bytes value']), - ([], []), - (None, [])] - - # WHEN: Calling recent_files_conv with the possible values - for value, expected_result in possible_values: - actual_result = recent_files_conv(value) - - # THEN: The actual result should be the same as the expected result - self.assertEqual(actual_result, expected_result) - def test_settings_basic(self): """ Test the Settings creation and its default usage diff --git a/tests/functional/openlp_core_lib/test_db.py b/tests/functional/openlp_core_lib/test_db.py index c36ade6a7..51aa08830 100644 --- a/tests/functional/openlp_core_lib/test_db.py +++ b/tests/functional/openlp_core_lib/test_db.py @@ -22,9 +22,7 @@ """ Package to test the openlp.core.lib package. """ -import os import shutil -from pathlib import Path from tempfile import mkdtemp from unittest import TestCase @@ -34,6 +32,7 @@ from sqlalchemy.pool import NullPool from sqlalchemy.orm.scoping import ScopedSession from sqlalchemy import MetaData +from openlp.core.common.path import Path from openlp.core.lib.db import init_db, get_upgrade_op, delete_database, upgrade_db from openlp.core.lib.projector import upgrade as pjlink_upgrade diff --git a/tests/functional/openlp_core_lib/test_lib.py b/tests/functional/openlp_core_lib/test_lib.py index eefc4ccd8..2056665f4 100644 --- a/tests/functional/openlp_core_lib/test_lib.py +++ b/tests/functional/openlp_core_lib/test_lib.py @@ -24,12 +24,12 @@ Package to test the openlp.core.lib package. """ import os from datetime import datetime, timedelta -from pathlib import Path from unittest import TestCase from unittest.mock import MagicMock, patch from PyQt5 import QtCore, QtGui +from openlp.core.common.path import Path from openlp.core.lib import FormattingTags, build_icon, check_item_selected, clean_tags, compare_chord_lyric, \ create_separated_list, create_thumb, expand_chords, expand_chords_for_printing, expand_tags, find_formatting_tags, \ get_text_file_string, image_to_byte, replace_params, resize_image, str_to_bool, validate_thumb diff --git a/tests/functional/openlp_core_lib/test_path.py b/tests/functional/openlp_core_lib/test_path.py index cf93e634c..f59a1ce08 100644 --- a/tests/functional/openlp_core_lib/test_path.py +++ b/tests/functional/openlp_core_lib/test_path.py @@ -23,10 +23,9 @@ Package to test the openlp.core.lib.path package. """ import os -from pathlib import Path from unittest import TestCase -from openlp.core.lib.path import path_to_str, str_to_path +from openlp.core.common.path import Path, path_to_str, str_to_path class TestPath(TestCase): diff --git a/tests/functional/openlp_core_ui/test_exceptionform.py b/tests/functional/openlp_core_ui/test_exceptionform.py index f205a4ce2..37a040aa6 100644 --- a/tests/functional/openlp_core_ui/test_exceptionform.py +++ b/tests/functional/openlp_core_ui/test_exceptionform.py @@ -29,6 +29,7 @@ from unittest import TestCase from unittest.mock import mock_open, patch from openlp.core.common import Registry +from openlp.core.common.path import Path from openlp.core.ui import exceptionform from tests.helpers.testmixin import TestMixin @@ -154,7 +155,7 @@ class TestExceptionForm(TestMixin, TestCase): # THEN: Verify strings were formatted properly mocked_add_query_item.assert_called_with('body', MAIL_ITEM_TEXT) - @patch("openlp.core.ui.exceptionform.QtWidgets.QFileDialog.getSaveFileName") + @patch("openlp.core.ui.exceptionform.FileDialog.getSaveFileName") @patch("openlp.core.ui.exceptionform.Qt") def test_on_save_report_button_clicked(self, mocked_qt, @@ -181,7 +182,7 @@ class TestExceptionForm(TestMixin, TestCase): mocked_qt.PYQT_VERSION_STR = 'PyQt5 Test' mocked_is_linux.return_value = False mocked_application_version.return_value = 'Trunk Test' - mocked_save_filename.return_value = ['testfile.txt', ] + mocked_save_filename.return_value = (Path('testfile.txt'), 'filter') test_form = exceptionform.ExceptionForm() test_form.file_attachment = None diff --git a/tests/functional/openlp_core_ui/test_firsttimeform.py b/tests/functional/openlp_core_ui/test_firsttimeform.py index f86ac9edf..0f7c6be6a 100644 --- a/tests/functional/openlp_core_ui/test_firsttimeform.py +++ b/tests/functional/openlp_core_ui/test_firsttimeform.py @@ -25,11 +25,11 @@ Package to test the openlp.core.ui.firsttimeform package. import os import tempfile import urllib -from pathlib import Path from unittest import TestCase from unittest.mock import MagicMock, patch from openlp.core.common import Registry +from openlp.core.common.path import Path from openlp.core.ui.firsttimeform import FirstTimeForm from tests.helpers.testmixin import TestMixin diff --git a/tests/functional/openlp_core_ui/test_themeform.py b/tests/functional/openlp_core_ui/test_themeform.py index 97152feed..cff097893 100644 --- a/tests/functional/openlp_core_ui/test_themeform.py +++ b/tests/functional/openlp_core_ui/test_themeform.py @@ -22,10 +22,10 @@ """ Package to test the openlp.core.ui.themeform package. """ -from pathlib import Path from unittest import TestCase from unittest.mock import MagicMock, patch +from openlp.core.common.path import Path from openlp.core.ui import ThemeForm diff --git a/tests/functional/openlp_core_ui_lib/test_filedialog.py b/tests/functional/openlp_core_ui_lib/test_filedialog.py index 6ec045d47..777ff65ec 100755 --- a/tests/functional/openlp_core_ui_lib/test_filedialog.py +++ b/tests/functional/openlp_core_ui_lib/test_filedialog.py @@ -1,10 +1,10 @@ import os from unittest import TestCase from unittest.mock import patch -from pathlib import Path from PyQt5 import QtWidgets +from openlp.core.common.path import Path from openlp.core.ui.lib.filedialog import FileDialog diff --git a/tests/functional/openlp_core_ui_lib/test_pathedit.py b/tests/functional/openlp_core_ui_lib/test_pathedit.py index 9ef4dff4b..227a4317a 100755 --- a/tests/functional/openlp_core_ui_lib/test_pathedit.py +++ b/tests/functional/openlp_core_ui_lib/test_pathedit.py @@ -23,10 +23,10 @@ This module contains tests for the openlp.core.ui.lib.pathedit module """ import os -from pathlib import Path from unittest import TestCase from unittest.mock import MagicMock, PropertyMock, patch +from openlp.core.common.path import Path from openlp.core.ui.lib import PathEdit, PathType from openlp.core.ui.lib.filedialog import FileDialog diff --git a/tests/functional/openlp_plugins/bibles/test_manager.py b/tests/functional/openlp_plugins/bibles/test_manager.py index de924a476..73e64d130 100644 --- a/tests/functional/openlp_plugins/bibles/test_manager.py +++ b/tests/functional/openlp_plugins/bibles/test_manager.py @@ -22,10 +22,10 @@ """ This module contains tests for the manager submodule of the Bibles plugin. """ -from pathlib import Path from unittest import TestCase from unittest.mock import MagicMock, patch +from openlp.core.common.path import Path from openlp.plugins.bibles.lib.manager import BibleManager diff --git a/tests/functional/openlp_plugins/images/test_lib.py b/tests/functional/openlp_plugins/images/test_lib.py index f00463d67..821a64bb0 100644 --- a/tests/functional/openlp_plugins/images/test_lib.py +++ b/tests/functional/openlp_plugins/images/test_lib.py @@ -28,6 +28,7 @@ from unittest.mock import ANY, MagicMock, patch from PyQt5 import QtCore, QtWidgets from openlp.core.common import Registry +from openlp.core.common.path import Path from openlp.plugins.images.lib.db import ImageFilenames, ImageGroups from openlp.plugins.images.lib.mediaitem import ImageMediaItem @@ -65,7 +66,7 @@ class TestImageMediaItem(TestCase): # THEN: load_list should have been called with the file list and None, # the directory should have been saved to the settings mocked_load_list.assert_called_once_with(file_list, None) - mocked_settings().setValue.assert_called_once_with(ANY, '/path1') + mocked_settings().setValue.assert_called_once_with(ANY, Path('/', 'path1')) @patch('openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_list') @patch('openlp.plugins.images.lib.mediaitem.Settings') @@ -82,7 +83,7 @@ class TestImageMediaItem(TestCase): # THEN: load_list should have been called with the file list and the group name, # the directory should have been saved to the settings mocked_load_list.assert_called_once_with(file_list, 'group') - mocked_settings().setValue.assert_called_once_with(ANY, '/path1') + mocked_settings().setValue.assert_called_once_with(ANY, Path('/', 'path1')) @patch('openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list') def test_save_new_images_list_empty_list(self, mocked_load_full_list): diff --git a/tests/functional/openlp_plugins/media/test_mediaitem.py b/tests/functional/openlp_plugins/media/test_mediaitem.py index 80d21da91..d0ed7499f 100644 --- a/tests/functional/openlp_plugins/media/test_mediaitem.py +++ b/tests/functional/openlp_plugins/media/test_mediaitem.py @@ -28,6 +28,7 @@ from unittest.mock import MagicMock, patch from PyQt5 import QtCore from openlp.core import Settings +from openlp.core.common.path import Path from openlp.plugins.media.lib.mediaitem import MediaMediaItem from tests.helpers.testmixin import TestMixin @@ -66,7 +67,7 @@ class MediaItemTest(TestCase, TestMixin): Media Remote Search Successful find """ # GIVEN: The Mediaitem set up a list of media - Settings().setValue(self.media_item.settings_section + '/media files', ['test.mp3', 'test.mp4']) + Settings().setValue(self.media_item.settings_section + '/media files', [Path('test.mp3'), Path('test.mp4')]) # WHEN: Retrieving the test file result = self.media_item.search('test.mp4', False) # THEN: a file should be found @@ -77,7 +78,7 @@ class MediaItemTest(TestCase, TestMixin): Media Remote Search not find """ # GIVEN: The Mediaitem set up a list of media - Settings().setValue(self.media_item.settings_section + '/media files', ['test.mp3', 'test.mp4']) + Settings().setValue(self.media_item.settings_section + '/media files', [Path('test.mp3'), Path('test.mp4')]) # WHEN: Retrieving the test file result = self.media_item.search('test.mpx', False) # THEN: a file should be found diff --git a/tests/functional/openlp_plugins/presentations/test_presentationcontroller.py b/tests/functional/openlp_plugins/presentations/test_presentationcontroller.py index fcd7b4f36..1bbd29522 100644 --- a/tests/functional/openlp_plugins/presentations/test_presentationcontroller.py +++ b/tests/functional/openlp_plugins/presentations/test_presentationcontroller.py @@ -24,10 +24,10 @@ Functional tests to test the PresentationController and PresentationDocument classes and related methods. """ import os -from pathlib import Path from unittest import TestCase from unittest.mock import MagicMock, mock_open, patch +from openlp.core.common.path import Path from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument FOLDER_TO_PATCH = 'openlp.plugins.presentations.lib.presentationcontroller.PresentationDocument.get_thumbnail_folder' diff --git a/tests/interfaces/openlp_core_common/test_utils.py b/tests/interfaces/openlp_core_common/test_utils.py index c7f74ddb5..9669e3038 100644 --- a/tests/interfaces/openlp_core_common/test_utils.py +++ b/tests/interfaces/openlp_core_common/test_utils.py @@ -22,10 +22,10 @@ """ Functional tests to test the AppLocation class and related methods. """ -from pathlib import Path from unittest import TestCase from openlp.core.common import is_not_image_file +from openlp.core.common.path import Path from tests.utils.constants import TEST_RESOURCES_PATH from tests.helpers.testmixin import TestMixin diff --git a/tests/interfaces/openlp_core_lib/test_pluginmanager.py b/tests/interfaces/openlp_core_lib/test_pluginmanager.py index 39e964143..2e9e8342f 100644 --- a/tests/interfaces/openlp_core_lib/test_pluginmanager.py +++ b/tests/interfaces/openlp_core_lib/test_pluginmanager.py @@ -32,6 +32,7 @@ from unittest.mock import MagicMock, patch from PyQt5 import QtWidgets from openlp.core.common import Registry, Settings +from openlp.core.common.path import Path from openlp.core.lib.pluginmanager import PluginManager from tests.helpers.testmixin import TestMixin @@ -48,7 +49,7 @@ class TestPluginManager(TestCase, TestMixin): """ self.setup_application() self.build_settings() - self.temp_dir = mkdtemp('openlp') + self.temp_dir = Path(mkdtemp('openlp')) Settings().setValue('advanced/data path', self.temp_dir) Registry.create() Registry().register('service_list', MagicMock()) @@ -62,7 +63,7 @@ class TestPluginManager(TestCase, TestMixin): # On windows we need to manually garbage collect to close sqlalchemy files # to avoid errors when temporary files are deleted. gc.collect() - shutil.rmtree(self.temp_dir) + shutil.rmtree(str(self.temp_dir)) @patch('openlp.plugins.songusage.lib.db.init_schema') @patch('openlp.plugins.songs.lib.db.init_schema')