← Back to team overview

openlp-core team mailing list archive

[Merge] lp:~phill-ridout/openlp/pathlib2 into lp:openlp

 

Phill has proposed merging lp:~phill-ridout/openlp/pathlib2 into lp:openlp.

Requested reviews:
  OpenLP Core (openlp-core)

For more details, see:
https://code.launchpad.net/~phill-ridout/openlp/pathlib2/+merge/328595

Part 2

Changed the pathedit widget over to using pathlib
Added a 'patched' file dialog
Added a few utility methods

lp:~phill-ridout/openlp/pathlib2 (revision 2758)
[SUCCESS] https://ci.openlp.io/job/Branch-01-Pull/2114/
[SUCCESS] https://ci.openlp.io/job/Branch-02-Functional-Tests/2024/
[SUCCESS] https://ci.openlp.io/job/Branch-03-Interface-Tests/1932/
[SUCCESS] https://ci.openlp.io/job/Branch-04a-Code_Analysis/1309/
[SUCCESS] https://ci.openlp.io/job/Branch-04b-Test_Coverage/1152/
[SUCCESS] https://ci.openlp.io/job/Branch-04c-Code_Analysis2/282/
[FAILURE] https://ci.openlp.io/job/Branch-05-AppVeyor-Tests/127/
Stopping after failure

-- 
Your team OpenLP Core is requested to review the proposed merge of lp:~phill-ridout/openlp/pathlib2 into lp:openlp.
=== added file 'openlp/core/common/path.py'
--- openlp/core/common/path.py	1970-01-01 00:00:00 +0000
+++ openlp/core/common/path.py	2017-08-04 18:15:25 +0000
@@ -0,0 +1,61 @@
+# -*- 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)

=== modified file 'openlp/core/lib/__init__.py'
--- openlp/core/lib/__init__.py	2017-05-20 05:51:58 +0000
+++ openlp/core/lib/__init__.py	2017-08-04 18:15:25 +0000
@@ -608,6 +608,41 @@
     return list_to_string
 
 
+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 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)
+
+
+    Usage:
+        Take a method with the following signature, and assume we which to apply the str function to arg2:
+            def method(arg1=None, arg2=None, arg3=None)
+
+        As arg2 can be specified postitionally as the second argument (1 with a zero index) or as a keyword, the we
+        would call this function as follows:
+
+        replace_params(args, kwargs, ((1, 'arg2', str),))
+    """
+    args = list(args)
+    for position, key_word, transform in params:
+        if len(args) > position:
+            args[position] = transform(args[position])
+        elif key_word in kwargs:
+            kwargs[key_word] = transform(kwargs[key_word])
+    return tuple(args), kwargs
+
+
 from .exceptions import ValidationError
 from .filedialog import FileDialog
 from .screen import ScreenList

=== modified file 'openlp/core/ui/advancedtab.py'
--- openlp/core/ui/advancedtab.py	2017-08-01 20:59:41 +0000
+++ openlp/core/ui/advancedtab.py	2017-08-04 18:15:25 +0000
@@ -30,6 +30,7 @@
 
 from openlp.core.common import AppLocation, Settings, SlideLimits, UiStrings, translate
 from openlp.core.common.languagemanager import format_time
+from openlp.core.common.path import path_to_str
 from openlp.core.lib import SettingsTab, build_icon
 from openlp.core.ui.lib import PathEdit, PathType
 
@@ -156,7 +157,7 @@
         self.data_directory_new_label = QtWidgets.QLabel(self.data_directory_group_box)
         self.data_directory_new_label.setObjectName('data_directory_current_label')
         self.data_directory_path_edit = PathEdit(self.data_directory_group_box, path_type=PathType.Directories,
-                                                 default_path=str(AppLocation.get_directory(AppLocation.DataDir)))
+                                                 default_path=AppLocation.get_directory(AppLocation.DataDir))
         self.data_directory_layout.addRow(self.data_directory_new_label, self.data_directory_path_edit)
         self.new_data_directory_has_files_label = QtWidgets.QLabel(self.data_directory_group_box)
         self.new_data_directory_has_files_label.setObjectName('new_data_directory_has_files_label')
@@ -373,7 +374,7 @@
         self.new_data_directory_has_files_label.hide()
         self.data_directory_cancel_button.hide()
         # Since data location can be changed, make sure the path is present.
-        self.data_directory_path_edit.path = str(AppLocation.get_data_path())
+        self.data_directory_path_edit.path = AppLocation.get_data_path()
         # Don't allow data directory move if running portable.
         if settings.value('advanced/is portable'):
             self.data_directory_group_box.hide()
@@ -497,12 +498,12 @@
                                                           'closed.').format(path=new_data_path),
                                                 defaultButton=QtWidgets.QMessageBox.No)
         if answer != QtWidgets.QMessageBox.Yes:
-            self.data_directory_path_edit.path = str(AppLocation.get_data_path())
+            self.data_directory_path_edit.path = AppLocation.get_data_path()
             return
         # Check if data already exists here.
-        self.check_data_overwrite(new_data_path)
+        self.check_data_overwrite(path_to_str(new_data_path))
         # Save the new location.
-        self.main_window.set_new_data_path(new_data_path)
+        self.main_window.set_new_data_path(path_to_str(new_data_path))
         self.data_directory_cancel_button.show()
 
     def on_data_directory_copy_check_box_toggled(self):
@@ -550,7 +551,7 @@
         """
         Cancel the data directory location change
         """
-        self.data_directory_path_edit.path = str(AppLocation.get_data_path())
+        self.data_directory_path_edit.path = AppLocation.get_data_path()
         self.data_directory_copy_check_box.setChecked(False)
         self.main_window.set_new_data_path(None)
         self.main_window.set_copy_data(False)

=== modified file 'openlp/core/ui/generaltab.py'
--- openlp/core/ui/generaltab.py	2017-05-22 19:56:54 +0000
+++ openlp/core/ui/generaltab.py	2017-08-04 18:15:25 +0000
@@ -23,6 +23,7 @@
 The general tab of the configuration dialog.
 """
 import logging
+from pathlib import Path
 
 from PyQt5 import QtCore, QtGui, QtWidgets
 
@@ -172,7 +173,8 @@
         self.logo_layout.setObjectName('logo_layout')
         self.logo_file_label = QtWidgets.QLabel(self.logo_group_box)
         self.logo_file_label.setObjectName('logo_file_label')
-        self.logo_file_path_edit = PathEdit(self.logo_group_box, default_path=':/graphics/openlp-splash-screen.png')
+        self.logo_file_path_edit = PathEdit(self.logo_group_box,
+                                            default_path=Path(':/graphics/openlp-splash-screen.png'))
         self.logo_layout.addRow(self.logo_file_label, self.logo_file_path_edit)
         self.logo_color_label = QtWidgets.QLabel(self.logo_group_box)
         self.logo_color_label.setObjectName('logo_color_label')
@@ -266,7 +268,7 @@
         self.audio_group_box.setTitle(translate('OpenLP.GeneralTab', 'Background Audio'))
         self.start_paused_check_box.setText(translate('OpenLP.GeneralTab', 'Start background audio paused'))
         self.repeat_list_check_box.setText(translate('OpenLP.GeneralTab', 'Repeat track list'))
-        self.logo_file_path_edit.dialog_caption = dialog_caption = translate('OpenLP.AdvancedTab', 'Select Logo File')
+        self.logo_file_path_edit.dialog_caption = translate('OpenLP.AdvancedTab', 'Select Logo File')
         self.logo_file_path_edit.filters = '{text};;{names} (*)'.format(
             text=get_images_filter(), names=UiStrings().AllFiles)
 
@@ -325,7 +327,7 @@
         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', self.logo_file_path_edit.path)
+        settings.setValue('logo file', str(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())

=== added file 'openlp/core/ui/lib/filedialogpatches.py'
--- openlp/core/ui/lib/filedialogpatches.py	1970-01-01 00:00:00 +0000
+++ openlp/core/ui/lib/filedialogpatches.py	2017-08-04 18:15:25 +0000
@@ -0,0 +1,118 @@
+# -*- 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                          #
+###############################################################################
+""" Patch the QFileDialog so it accepts and returns Path objects"""
+from functools import wraps
+from pathlib import Path
+
+from PyQt5 import QtWidgets
+
+from openlp.core.common.path import path_to_str, str_to_path
+from openlp.core.lib import replace_params
+
+
+class PQFileDialog(QtWidgets.QFileDialog):
+    @classmethod
+    @wraps(QtWidgets.QFileDialog.getExistingDirectory)
+    def getExistingDirectory(cls, *args, **kwargs):
+        """
+        Reimplement `getExistingDirectory` so that it can be called with, and return Path objects
+
+        :type parent: QtWidgets.QWidget or None
+        :type caption: str
+        :type directory: pathlib.Path
+        :type options: QtWidgets.QFileDialog.Options
+        :rtype: tuple[Path, str]
+        """
+
+        args, kwargs = replace_params(args, kwargs, ((2, 'directory', path_to_str),))
+
+        return_value = super().getExistingDirectory(*args, **kwargs)
+
+        # getExistingDirectory returns a str that represents the path. The string is empty if the user cancels the
+        # dialog.
+        return str_to_path(return_value)
+
+    @classmethod
+    @wraps(QtWidgets.QFileDialog.getOpenFileName)
+    def getOpenFileName(cls, *args, **kwargs):
+        """
+        Reimplement `getOpenFileName` so that it can be called with, and return Path objects
+
+        :type parent: QtWidgets.QWidget or None
+        :type caption: str
+        :type directory: pathlib.Path
+        :type filter: str
+        :type initialFilter: str
+        :type options: QtWidgets.QFileDialog.Options
+        :rtype: tuple[Path, str]
+        """
+
+        args, kwargs = replace_params(args, kwargs, ((2, 'directory', path_to_str),))
+
+        return_value = super().getOpenFileName(*args, **kwargs)
+
+        # getOpenFileName returns a tuple. The first item is a of str's that represents the path. The string is empty if
+        # the user cancels the dialog.
+        return str_to_path(return_value[0]), return_value[1]
+
+    @classmethod
+    def getOpenFileNames(cls, *args, **kwargs):
+        """
+        Reimplement `getOpenFileNames` so that it can be called with, and return Path objects
+
+        :type parent: QtWidgets.QWidget or None
+        :type caption: str
+        :type directory: pathlib.Path
+        :type filter: str
+        :type initialFilter: str
+        :type options: QtWidgets.QFileDialog.Options
+        :rtype: tuple[list[Path], str]
+        """
+        args, kwargs = replace_params(args, kwargs, ((2, 'directory', path_to_str),))
+
+        return_value = super().getOpenFileNames(*args, **kwargs)
+
+        # getSaveFileName returns a tuple. The first item is a list of str's that represents the path. The list is
+        # empty if the user cancels the dialog.
+        paths = [str_to_path(path) for path in return_value[0]]
+        return paths, return_value[1]
+
+    @classmethod
+    def getSaveFileName(cls, *args, **kwargs):
+        """
+        Reimplement `getSaveFileName` so that it can be called with, and return Path objects
+
+        :type parent: QtWidgets.QWidget or None
+        :type caption: str
+        :type directory: pathlib.Path
+        :type filter: str
+        :type initialFilter: str
+        :type options: QtWidgets.QFileDialog.Options
+        :rtype: tuple[Path or None, str]
+        """
+        args, kwargs = replace_params(args, kwargs, ((2, 'directory', path_to_str),))
+
+        return_value = super().getSaveFileName(*args, **kwargs)
+
+        # getSaveFileName returns a tuple. The first item represents the path as a str. The string is empty if the user
+        # cancels the dialog.
+        return str_to_path(return_value[0]), return_value[1]

=== modified file 'openlp/core/ui/lib/pathedit.py' (properties changed: +x to -x)
--- openlp/core/ui/lib/pathedit.py	2017-07-04 23:13:51 +0000
+++ openlp/core/ui/lib/pathedit.py	2017-08-04 18:15:25 +0000
@@ -20,12 +20,14 @@
 # Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
 ###############################################################################
 from enum import Enum
-import os.path
+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.lib import build_icon
+from openlp.core.ui.lib.filedialogpatches import PQFileDialog
 
 
 class PathType(Enum):
@@ -38,11 +40,12 @@
     The :class:`~openlp.core.ui.lib.pathedit.PathEdit` class subclasses QWidget to create a custom widget for use when
     a file or directory needs to be selected.
     """
-    pathChanged = QtCore.pyqtSignal(str)
+
+    pathChanged = QtCore.pyqtSignal(Path)
 
     def __init__(self, parent=None, path_type=PathType.Files, default_path=None, dialog_caption=None, show_revert=True):
         """
-        Initalise the PathEdit widget
+        Initialise the PathEdit widget
 
         :param parent: The parent of the widget. This is just passed to the super method.
         :type parent: QWidget or None
@@ -51,9 +54,9 @@
         :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: str
+        :type default_path: pathlib.Path
 
-        :param show_revert: Used to determin if the 'revert button' should be visible.
+        :param show_revert: Used to determine if the 'revert button' should be visible.
         :type show_revert: bool
 
         :return: None
@@ -79,7 +82,6 @@
         widget_layout = QtWidgets.QHBoxLayout()
         widget_layout.setContentsMargins(0, 0, 0, 0)
         self.line_edit = QtWidgets.QLineEdit(self)
-        self.line_edit.setText(self._path)
         widget_layout.addWidget(self.line_edit)
         self.browse_button = QtWidgets.QToolButton(self)
         self.browse_button.setIcon(build_icon(':/general/general_open.png'))
@@ -101,7 +103,7 @@
         A property getter method to return the selected path.
 
         :return: The selected path
-        :rtype: str
+        :rtype: pathlib.Path
         """
         return self._path
 
@@ -111,11 +113,15 @@
         A Property setter method to set the selected path
 
         :param path: The path to set the widget to
-        :type path: str
+        :type path: pathlib.Path
+
+        :return: None
+        :rtype: None
         """
         self._path = path
-        self.line_edit.setText(path)
-        self.line_edit.setToolTip(path)
+        text = path_to_str(path)
+        self.line_edit.setText(text)
+        self.line_edit.setToolTip(text)
 
     @property
     def path_type(self):
@@ -124,7 +130,7 @@
         selecting a file or directory.
 
         :return: The type selected
-        :rtype: Enum of PathEdit
+        :rtype: PathType
         """
         return self._path_type
 
@@ -133,8 +139,11 @@
         """
         A Property setter method to set the path type
 
-        :param path: The type of path to select
-        :type path: Enum of PathEdit
+        :param path_type: The type of path to select
+        :type path_type: PathType
+
+        :return: None
+        :rtype: None
         """
         self._path_type = path_type
         self.update_button_tool_tips()
@@ -142,7 +151,9 @@
     def update_button_tool_tips(self):
         """
         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:
             self.browse_button.setToolTip(translate('OpenLP.PathEdit', 'Browse for directory.'))
@@ -156,21 +167,21 @@
         A handler to handle a click on the browse button.
 
         Show the QFileDialog and process the input from the user
+
         :return: None
+        :rtype: None
         """
         caption = self.dialog_caption
-        path = ''
+        path = None
         if self._path_type == PathType.Directories:
             if not caption:
                 caption = translate('OpenLP.PathEdit', 'Select Directory')
-            path = QtWidgets.QFileDialog.getExistingDirectory(self, caption,
-                                                              self._path, QtWidgets.QFileDialog.ShowDirsOnly)
+            path = PQFileDialog.getExistingDirectory(self, caption, self._path, PQFileDialog.ShowDirsOnly)
         elif self._path_type == PathType.Files:
             if not caption:
                 caption = self.dialog_caption = translate('OpenLP.PathEdit', 'Select File')
-            path, filter_used = QtWidgets.QFileDialog.getOpenFileName(self, caption, self._path, self.filters)
+            path, filter_used = PQFileDialog.getOpenFileName(self, caption, self._path, self.filters)
         if path:
-            path = os.path.normpath(path)
             self.on_new_path(path)
 
     def on_revert_button_clicked(self):
@@ -178,16 +189,21 @@
         A handler to handle a click on the revert button.
 
         Set the new path to the value of the default_path instance variable.
+
         :return: None
+        :rtype: None
         """
         self.on_new_path(self.default_path)
 
     def on_line_edit_editing_finished(self):
         """
         A handler to handle when the line edit has finished being edited.
+
         :return: None
+        :rtype: None
         """
-        self.on_new_path(self.line_edit.text())
+        path = str_to_path(self.line_edit.text())
+        self.on_new_path(path)
 
     def on_new_path(self, path):
         """
@@ -196,9 +212,10 @@
         Emits the pathChanged Signal
 
         :param path: The new path
-        :type path: str
+        :type path: pathlib.Path
 
         :return: None
+        :rtype: None
         """
         if self._path != path:
             self.path = path

=== modified file 'openlp/core/ui/themeform.py'
--- openlp/core/ui/themeform.py	2017-05-30 18:50:39 +0000
+++ openlp/core/ui/themeform.py	2017-08-04 18:15:25 +0000
@@ -28,12 +28,15 @@
 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
 from openlp.core.lib.theme import BackgroundType, BackgroundGradientType
 from openlp.core.lib.ui import critical_error_message_box
 from openlp.core.ui import ThemeLayoutForm
 from openlp.core.ui.media.webkitplayer import VIDEO_EXT
 from .themewizard import Ui_ThemeWizard
 
+from pathlib import Path
+
 log = logging.getLogger(__name__)
 
 
@@ -316,11 +319,11 @@
             self.setField('background_type', 1)
         elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Image):
             self.image_color_button.color = self.theme.background_border_color
-            self.image_path_edit.path = self.theme.background_filename
+            self.image_path_edit.path = path_to_str(self.theme.background_filename)
             self.setField('background_type', 2)
         elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Video):
             self.video_color_button.color = self.theme.background_border_color
-            self.video_path_edit.path = self.theme.background_filename
+            self.video_path_edit.path = path_to_str(self.theme.background_filename)
             self.setField('background_type', 4)
         elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Transparent):
             self.setField('background_type', 3)
@@ -448,18 +451,18 @@
         """
         self.theme.background_end_color = color
 
-    def on_image_path_edit_path_changed(self, filename):
+    def on_image_path_edit_path_changed(self, file_path):
         """
         Background Image button pushed.
         """
-        self.theme.background_filename = filename
+        self.theme.background_filename = path_to_str(file_path)
         self.set_background_page_values()
 
-    def on_video_path_edit_path_changed(self, filename):
+    def on_video_path_edit_path_changed(self, file_path):
         """
         Background video button pushed.
         """
-        self.theme.background_filename = filename
+        self.theme.background_filename = path_to_str(file_path)
         self.set_background_page_values()
 
     def on_main_color_changed(self, color):

=== modified file 'openlp/core/ui/themewizard.py'
--- openlp/core/ui/themewizard.py	2017-05-22 18:22:43 +0000
+++ openlp/core/ui/themewizard.py	2017-08-04 18:15:25 +0000
@@ -30,6 +30,8 @@
 from openlp.core.lib.ui import add_welcome_page, create_valign_selection_widgets
 from openlp.core.ui.lib import ColorButton, PathEdit
 
+from pathlib import Path
+
 
 class Ui_ThemeWizard(object):
     """

=== modified file 'openlp/plugins/bibles/lib/importers/http.py'
--- openlp/plugins/bibles/lib/importers/http.py	2017-08-02 06:10:26 +0000
+++ openlp/plugins/bibles/lib/importers/http.py	2017-08-04 18:15:25 +0000
@@ -255,7 +255,7 @@
                                                                         chapter=chapter,
                                                                         version=version)
         soup = get_soup_for_bible_ref(
-            'http://biblegateway.com/passage/?{url}'.format(url=url_params),
+            'http://www.biblegateway.com/passage/?{url}'.format(url=url_params),
             pre_parse_regex=r'<meta name.*?/>', pre_parse_substitute='')
         if not soup:
             return None
@@ -284,7 +284,7 @@
         """
         log.debug('BGExtract.get_books_from_http("{version}")'.format(version=version))
         url_params = urllib.parse.urlencode({'action': 'getVersionInfo', 'vid': '{version}'.format(version=version)})
-        reference_url = 'http://biblegateway.com/versions/?{url}#books'.format(url=url_params)
+        reference_url = 'http://www.biblegateway.com/versions/?{url}#books'.format(url=url_params)
         page = get_web_page(reference_url)
         if not page:
             send_error_message('download')

=== modified file 'openlp/plugins/presentations/lib/presentationtab.py'
--- openlp/plugins/presentations/lib/presentationtab.py	2017-05-22 18:27:40 +0000
+++ openlp/plugins/presentations/lib/presentationtab.py	2017-08-04 18:15:25 +0000
@@ -20,10 +20,11 @@
 # Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
 ###############################################################################
 
-from PyQt5 import QtGui, QtWidgets
+from PyQt5 import QtWidgets
 
 from openlp.core.common import Settings, UiStrings, translate
-from openlp.core.lib import SettingsTab, build_icon
+from openlp.core.common.path import path_to_str, str_to_path
+from openlp.core.lib import SettingsTab
 from openlp.core.lib.ui import critical_error_message_box
 from openlp.core.ui.lib import PathEdit
 from openlp.plugins.presentations.lib.pdfcontroller import PdfController
@@ -156,7 +157,7 @@
         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 = pdf_program
+            self.program_path_edit.path = str_to_path(pdf_program)
 
     def save(self):
         """
@@ -192,7 +193,7 @@
             Settings().setValue(setting_key, self.ppt_window_check_box.checkState())
             changed = True
         # Save pdf-settings
-        pdf_program = self.program_path_edit.path
+        pdf_program = path_to_str(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 == '':
@@ -219,12 +220,12 @@
             checkbox.setEnabled(controller.is_available())
             self.set_controller_text(checkbox, controller)
 
-    def on_program_path_edit_path_changed(self, filename):
+    def on_program_path_edit_path_changed(self, new_path):
         """
         Select the mudraw or ghostscript binary that should be used.
         """
-        if filename:
-            if not PdfController.process_check_binary(filename):
+        if new_path:
+            if not PdfController.process_check_binary(new_path):
                 critical_error_message_box(UiStrings().Error,
                                            translate('PresentationPlugin.PresentationTab',
                                                      'The program is not ghostscript or mudraw which is required.'))

=== modified file 'openlp/plugins/songusage/forms/songusagedetailform.py'
--- openlp/plugins/songusage/forms/songusagedetailform.py	2017-06-04 12:14:23 +0000
+++ openlp/plugins/songusage/forms/songusagedetailform.py	2017-08-04 18:15:25 +0000
@@ -27,6 +27,7 @@
 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.lib.ui import critical_error_message_box
 from openlp.plugins.songusage.lib.db import SongUsageItem
 from .songusagedetaildialog import Ui_SongUsageDetailDialog
@@ -55,20 +56,21 @@
         """
         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 = Settings().value(self.plugin.settings_section + '/last directory export')
+        self.report_path_edit.path = str_to_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
         """
-        Settings().setValue(self.plugin.settings_section + '/last directory export', file_path)
+        Settings().setValue(self.plugin.settings_section + '/last directory export', path_to_str(file_path))
 
     def accept(self):
         """
         Ok was triggered so lets save the data and run the report
         """
         log.debug('accept')
-        path = self.report_path_edit.path
+        path = path_to_str(self.report_path_edit.path)
         if not path:
             self.main_window.error_message(
                 translate('SongUsagePlugin.SongUsageDetailForm', 'Output Path Not Selected'),

=== added file 'tests/functional/openlp_core_common/test_path.py'
--- tests/functional/openlp_core_common/test_path.py	1970-01-01 00:00:00 +0000
+++ tests/functional/openlp_core_common/test_path.py	2017-08-04 18:15:25 +0000
@@ -0,0 +1,88 @@
+# -*- 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.path package.
+"""
+import os
+from pathlib import Path
+from unittest import TestCase
+
+from openlp.core.common.path import path_to_str, str_to_path
+
+
+class TestPath(TestCase):
+    """
+    Tests for the :mod:`openlp.core.common.path` module
+    """
+
+    def test_path_to_str_type_error(self):
+        """
+        Test that `path_to_str` raises a type error when called with an invalid type
+        """
+        # GIVEN: The `path_to_str` function
+        # WHEN: Calling `path_to_str` with an invalid Type
+        # THEN: A TypeError should have been raised
+        with self.assertRaises(TypeError):
+            path_to_str(str())
+
+    def test_path_to_str_none(self):
+        """
+        Test that `path_to_str` correctly converts the path parameter when passed with None
+        """
+        # GIVEN: The `path_to_str` function
+        # WHEN: Calling the `path_to_str` function with None
+        result = path_to_str(None)
+
+        # THEN: `path_to_str` should return an empty string
+        self.assertEqual(result, '')
+
+    def test_path_to_str_path_object(self):
+        """
+        Test that `path_to_str` correctly converts the path parameter when passed a Path object
+        """
+        # GIVEN: The `path_to_str` function
+        # WHEN: Calling the `path_to_str` function with a Path object
+        result = path_to_str(Path('test/path'))
+
+        # THEN: `path_to_str` should return a string representation of the Path object
+        self.assertEqual(result, os.path.join('test', 'path'))
+
+    def test_str_to_path_type_error(self):
+        """
+        Test that `str_to_path` raises a type error when called with an invalid type
+        """
+        # GIVEN: The `str_to_path` function
+        # WHEN: Calling `str_to_path` with an invalid Type
+        # THEN: A TypeError should have been raised
+        with self.assertRaises(TypeError):
+            str_to_path(Path())
+
+    def test_str_to_path_empty_str(self):
+        """
+        Test that `str_to_path` correctly converts the string parameter when passed with and empty string
+        """
+        # GIVEN: The `str_to_path` function
+        # WHEN: Calling the `str_to_path` function with None
+        result = str_to_path('')
+
+        # THEN: `path_to_str` should return None
+        self.assertEqual(result, None)

=== modified file 'tests/functional/openlp_core_lib/test_lib.py'
--- tests/functional/openlp_core_lib/test_lib.py	2017-05-17 20:06:45 +0000
+++ tests/functional/openlp_core_lib/test_lib.py	2017-08-04 18:15:25 +0000
@@ -29,10 +29,9 @@
 
 from PyQt5 import QtCore, QtGui
 
-from openlp.core.lib import FormattingTags, expand_chords_for_printing
-from openlp.core.lib import build_icon, check_item_selected, clean_tags, create_thumb, create_separated_list, \
-    expand_tags, get_text_file_string, image_to_byte, resize_image, str_to_bool, validate_thumb, expand_chords, \
-    compare_chord_lyric, find_formatting_tags
+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
 
 TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'resources'))
 
@@ -652,6 +651,38 @@
             mocked_os.stat.assert_any_call(thumb_path)
             assert result is False, 'The result should be False'
 
+    def test_replace_params_no_params(self):
+        """
+        Test replace_params when called with and empty tuple instead of parameters to replace
+        """
+        # GIVEN: Some test data
+        test_args = (1, 2)
+        test_kwargs = {'arg3': 3, 'arg4': 4}
+        test_params = tuple()
+
+        # WHEN: Calling replace_params
+        result_args, result_kwargs = replace_params(test_args, test_kwargs, test_params)
+
+        # THEN: The positional and keyword args should not have changed
+        self.assertEqual(test_args, result_args)
+        self.assertEqual(test_kwargs, result_kwargs)
+
+    def test_replace_params_params(self):
+        """
+        Test replace_params when given a positional and a keyword argument to change
+        """
+        # GIVEN: Some test data
+        test_args = (1, 2)
+        test_kwargs = {'arg3': 3, 'arg4': 4}
+        test_params = ((1, 'arg2', str), (2, 'arg3', str))
+
+        # WHEN: Calling replace_params
+        result_args, result_kwargs = replace_params(test_args, test_kwargs, test_params)
+
+        # THEN: The positional and keyword args should have have changed
+        self.assertEqual(result_args, (1, '2'))
+        self.assertEqual(result_kwargs, {'arg3': '3', 'arg4': 4})
+
     def test_resize_thumb(self):
         """
         Test the resize_thumb() function

=== modified file 'tests/functional/openlp_core_ui/test_themeform.py'
--- tests/functional/openlp_core_ui/test_themeform.py	2017-05-14 07:15:29 +0000
+++ tests/functional/openlp_core_ui/test_themeform.py	2017-08-04 18:15:25 +0000
@@ -22,6 +22,7 @@
 """
 Package to test the openlp.core.ui.themeform package.
 """
+from pathlib import Path
 from unittest import TestCase
 from unittest.mock import MagicMock, patch
 
@@ -45,7 +46,7 @@
             self.instance.theme = MagicMock()
 
             # WHEN: `on_image_path_edit_path_changed` is clicked
-            self.instance.on_image_path_edit_path_changed('/new/pat.h')
+            self.instance.on_image_path_edit_path_changed(Path('/', 'new', 'pat.h'))
 
             # THEN: The theme background file should be set and `set_background_page_values` should have been called
             self.assertEqual(self.instance.theme.background_filename, '/new/pat.h')

=== renamed file 'tests/functional/openlp_core_ui_lib/test_color_button.py' => 'tests/functional/openlp_core_ui_lib/test_colorbutton.py'
=== added file 'tests/functional/openlp_core_ui_lib/test_filedialogpatches.py'
--- tests/functional/openlp_core_ui_lib/test_filedialogpatches.py	1970-01-01 00:00:00 +0000
+++ tests/functional/openlp_core_ui_lib/test_filedialogpatches.py	2017-08-04 18:15:25 +0000
@@ -0,0 +1,189 @@
+import os
+import sys
+from unittest import TestCase
+from unittest.mock import patch
+from pathlib import Path
+
+from PyQt5 import QtWidgets
+
+from openlp.core.ui.lib.filedialogpatches import PQFileDialog
+
+
+class TestFileDialogPatches(TestCase):
+    """
+    Tests for the :mod:`openlp.core.ui.lib.filedialogpatches` module
+    """
+
+    def test_pq_file_dialog(self):
+        """
+        Test that the :class:`PQFileDialog` instantiates correctly
+        """
+        # GIVEN: The PQFileDialog class
+        # WHEN: Creating an instance
+        instance = PQFileDialog()
+
+        # THEN: The instance should be an instance of QFileDialog
+        self.assertIsInstance(instance, QtWidgets.QFileDialog)
+
+    def test_get_existing_directory_user_abort(self):
+        """
+        Test that `getExistingDirectory` handles the case when the user cancels the dialog
+        """
+        # GIVEN: PQFileDialog with a mocked QDialog.getExistingDirectory method
+        # WHEN: Calling PQFileDialog.getExistingDirectory and the user cancels the dialog returns a empty string
+        with patch('PyQt5.QtWidgets.QFileDialog.getExistingDirectory', return_value=''):
+            result = PQFileDialog.getExistingDirectory()
+
+            # THEN: The result should be None
+            self.assertEqual(result, None)
+
+    def test_get_existing_directory_user_accepts(self):
+        """
+        Test that `getExistingDirectory` handles the case when the user accepts the dialog
+        """
+        # GIVEN: PQFileDialog with a mocked QDialog.getExistingDirectory method
+        # WHEN: Calling PQFileDialog.getExistingDirectory, the user chooses a file and accepts the dialog (it returns a
+        #       string pointing to the directory)
+        with patch('PyQt5.QtWidgets.QFileDialog.getExistingDirectory', return_value=os.path.join('test', 'dir')):
+            result = PQFileDialog.getExistingDirectory()
+
+            # THEN: getExistingDirectory() should return a Path object pointing to the chosen file
+            self.assertEqual(result, Path('test', 'dir'))
+
+    def test_get_existing_directory_param_order(self):
+        """
+        Test that `getExistingDirectory` passes the parameters to `QFileDialog.getExistingDirectory` in the correct
+        order
+        """
+        # GIVEN: PQFileDialog
+        with patch('openlp.core.ui.lib.filedialogpatches.QtWidgets.QFileDialog.getExistingDirectory', return_value='') \
+                as mocked_get_existing_directory:
+
+            # WHEN: Calling the getExistingDirectory method with all parameters set
+            PQFileDialog.getExistingDirectory('Parent', 'Caption', Path('test', 'dir'), 'Options')
+
+            # THEN: The `QFileDialog.getExistingDirectory` should have been called with the parameters in the correct
+            #       order
+            mocked_get_existing_directory.assert_called_once_with('Parent', 'Caption', os.path.join('test', 'dir'),
+                                                                  'Options')
+
+    def test_get_open_file_name_user_abort(self):
+        """
+        Test that `getOpenFileName` handles the case when the user cancels the dialog
+        """
+        # GIVEN: PQFileDialog with a mocked QDialog.getOpenFileName method
+        # WHEN: Calling PQFileDialog.getOpenFileName and the user cancels the dialog (it returns a tuple with the first
+        #       value set as an empty string)
+        with patch('PyQt5.QtWidgets.QFileDialog.getOpenFileName', return_value=('', '')):
+            result = PQFileDialog.getOpenFileName()
+
+            # THEN: First value should be None
+            self.assertEqual(result[0], None)
+
+    def test_get_open_file_name_user_accepts(self):
+        """
+        Test that `getOpenFileName` handles the case when the user accepts the dialog
+        """
+        # GIVEN: PQFileDialog with a mocked QDialog.getOpenFileName method
+        # WHEN: Calling PQFileDialog.getOpenFileName, the user chooses a file and accepts the dialog (it returns a
+        #       tuple with the first value set as an string pointing to the file)
+        with patch('PyQt5.QtWidgets.QFileDialog.getOpenFileName',
+                   return_value=(os.path.join('test', 'chosen.file'), '')):
+            result = PQFileDialog.getOpenFileName()
+
+            # THEN: getOpenFileName() should return a tuple with the first value set to a Path object pointing to the
+            #       chosen file
+            self.assertEqual(result[0], Path('test', 'chosen.file'))
+
+    def test_get_open_file_name_selected_filter(self):
+        """
+        Test that `getOpenFileName` does not modify the selectedFilter as returned by `QFileDialog.getOpenFileName`
+        """
+        # GIVEN: PQFileDialog with a mocked QDialog.get_save_file_name method
+        # WHEN: Calling PQFileDialog.getOpenFileName, and `QFileDialog.getOpenFileName` returns a known `selectedFilter`
+        with patch('PyQt5.QtWidgets.QFileDialog.getOpenFileName', return_value=('', 'selected filter')):
+            result = PQFileDialog.getOpenFileName()
+
+            # THEN: getOpenFileName() should return a tuple with the second value set to a the selected filter
+            self.assertEqual(result[1], 'selected filter')
+
+    def test_get_open_file_names_user_abort(self):
+        """
+        Test that `getOpenFileNames` handles the case when the user cancels the dialog
+        """
+        # GIVEN: PQFileDialog with a mocked QDialog.getOpenFileNames method
+        # WHEN: Calling PQFileDialog.getOpenFileNames and the user cancels the dialog (it returns a tuple with the first
+        #       value set as an empty list)
+        with patch('PyQt5.QtWidgets.QFileDialog.getOpenFileNames', return_value=([], '')):
+            result = PQFileDialog.getOpenFileNames()
+
+            # THEN: First value should be an empty list
+            self.assertEqual(result[0], [])
+
+    def test_get_open_file_names_user_accepts(self):
+        """
+        Test that `getOpenFileNames` handles the case when the user accepts the dialog
+        """
+        # GIVEN: PQFileDialog with a mocked QDialog.getOpenFileNames method
+        # WHEN: Calling PQFileDialog.getOpenFileNames, the user chooses some files and accepts the dialog (it returns a
+        #       tuple with the first value set as a list of strings pointing to the file)
+        with patch('PyQt5.QtWidgets.QFileDialog.getOpenFileNames',
+                   return_value=([os.path.join('test', 'chosen.file1'), os.path.join('test', 'chosen.file2')], '')):
+            result = PQFileDialog.getOpenFileNames()
+
+            # THEN: getOpenFileNames() should return a tuple with the first value set to a list of Path objects pointing
+            #       to the chosen file
+            self.assertEqual(result[0], [Path('test', 'chosen.file1'), Path('test', 'chosen.file2')])
+
+    def test_get_open_file_names_selected_filter(self):
+        """
+        Test that `getOpenFileNames` does not modify the selectedFilter as returned by `QFileDialog.getOpenFileNames`
+        """
+        # GIVEN: PQFileDialog with a mocked QDialog.getOpenFileNames method
+        # WHEN: Calling PQFileDialog.getOpenFileNames, and `QFileDialog.getOpenFileNames` returns a known
+        #       `selectedFilter`
+        with patch('PyQt5.QtWidgets.QFileDialog.getOpenFileNames', return_value=([], 'selected filter')):
+            result = PQFileDialog.getOpenFileNames()
+
+            # THEN: getOpenFileNames() should return a tuple with the second value set to a the selected filter
+            self.assertEqual(result[1], 'selected filter')
+
+    def test_get_save_file_name_user_abort(self):
+        """
+        Test that `getSaveFileName` handles the case when the user cancels the dialog
+        """
+        # GIVEN: PQFileDialog with a mocked QDialog.get_save_file_name method
+        # WHEN: Calling PQFileDialog.getSaveFileName and the user cancels the dialog (it returns a tuple with the first
+        #       value set as an empty string)
+        with patch('PyQt5.QtWidgets.QFileDialog.getSaveFileName', return_value=('', '')):
+            result = PQFileDialog.getSaveFileName()
+
+            # THEN: First value should be None
+            self.assertEqual(result[0], None)
+
+    def test_get_save_file_name_user_accepts(self):
+        """
+        Test that `getSaveFileName` handles the case when the user accepts the dialog
+        """
+        # GIVEN: PQFileDialog with a mocked QDialog.getSaveFileName method
+        # WHEN: Calling PQFileDialog.getSaveFileName, the user chooses a file and accepts the dialog (it returns a
+        #       tuple with the first value set as an string pointing to the file)
+        with patch('PyQt5.QtWidgets.QFileDialog.getSaveFileName',
+                   return_value=(os.path.join('test', 'chosen.file'), '')):
+            result = PQFileDialog.getSaveFileName()
+
+            # THEN: getSaveFileName() should return a tuple with the first value set to a Path object pointing to the
+            #       chosen file
+            self.assertEqual(result[0], Path('test', 'chosen.file'))
+
+    def test_get_save_file_name_selected_filter(self):
+        """
+        Test that `getSaveFileName` does not modify the selectedFilter as returned by `QFileDialog.getSaveFileName`
+        """
+        # GIVEN: PQFileDialog with a mocked QDialog.get_save_file_name method
+        # WHEN: Calling PQFileDialog.getSaveFileName, and `QFileDialog.getSaveFileName` returns a known `selectedFilter`
+        with patch('PyQt5.QtWidgets.QFileDialog.getSaveFileName', return_value=('', 'selected filter')):
+            result = PQFileDialog.getSaveFileName()
+
+            # THEN: getSaveFileName() should return a tuple with the second value set to a the selected filter
+            self.assertEqual(result[1], 'selected filter')

=== renamed file 'tests/functional/openlp_core_ui_lib/test_path_edit.py' => 'tests/functional/openlp_core_ui_lib/test_pathedit.py'
--- tests/functional/openlp_core_ui_lib/test_path_edit.py	2017-05-13 07:35:39 +0000
+++ tests/functional/openlp_core_ui_lib/test_pathedit.py	2017-08-04 18:15:25 +0000
@@ -22,12 +22,13 @@
 """
 This module contains tests for the openlp.core.ui.lib.pathedit module
 """
+import os
+from pathlib import Path
 from unittest import TestCase
-
-from PyQt5 import QtWidgets
+from unittest.mock import MagicMock, PropertyMock, patch
 
 from openlp.core.ui.lib import PathEdit, PathType
-from unittest.mock import MagicMock, PropertyMock, patch
+from openlp.core.ui.lib.filedialogpatches import PQFileDialog
 
 
 class TestPathEdit(TestCase):
@@ -43,11 +44,11 @@
         Test the `path` property getter.
         """
         # GIVEN: An instance of PathEdit with the `_path` instance variable set
-        self.widget._path = 'getter/test/pat.h'
+        self.widget._path = Path('getter', 'test', 'pat.h')
 
         # WHEN: Reading the `path` property
         # THEN: The value that we set should be returned
-        self.assertEqual(self.widget.path, 'getter/test/pat.h')
+        self.assertEqual(self.widget.path, Path('getter', 'test', 'pat.h'))
 
     def test_path_setter(self):
         """
@@ -57,13 +58,13 @@
         self.widget.line_edit = MagicMock()
 
         # WHEN: Writing to the `path` property
-        self.widget.path = 'setter/test/pat.h'
+        self.widget.path = Path('setter', 'test', 'pat.h')
 
         # THEN: The `_path` instance variable should be set with the test data. The `line_edit` text and tooltip
         #       should have also been set.
-        self.assertEqual(self.widget._path, 'setter/test/pat.h')
-        self.widget.line_edit.setToolTip.assert_called_once_with('setter/test/pat.h')
-        self.widget.line_edit.setText.assert_called_once_with('setter/test/pat.h')
+        self.assertEqual(self.widget._path, Path('setter', 'test', 'pat.h'))
+        self.widget.line_edit.setToolTip.assert_called_once_with(os.path.join('setter', 'test', 'pat.h'))
+        self.widget.line_edit.setText.assert_called_once_with(os.path.join('setter', 'test', 'pat.h'))
 
     def test_path_type_getter(self):
         """
@@ -125,22 +126,20 @@
         """
         # GIVEN: An instance of PathEdit with the `path_type` set to `Directories` and a mocked
         #        QFileDialog.getExistingDirectory
-        with patch('openlp.core.ui.lib.pathedit.QtWidgets.QFileDialog.getExistingDirectory', return_value='') as \
+        with patch('openlp.core.ui.lib.pathedit.PQFileDialog.getExistingDirectory', return_value=None) as \
                 mocked_get_existing_directory, \
-                patch('openlp.core.ui.lib.pathedit.QtWidgets.QFileDialog.getOpenFileName') as \
-                mocked_get_open_file_name, \
-                patch('openlp.core.ui.lib.pathedit.os.path.normpath') as mocked_normpath:
+                patch('openlp.core.ui.lib.pathedit.PQFileDialog.getOpenFileName') as mocked_get_open_file_name:
             self.widget._path_type = PathType.Directories
-            self.widget._path = 'test/path/'
+            self.widget._path = Path('test', 'path')
 
             # WHEN: Calling on_browse_button_clicked
             self.widget.on_browse_button_clicked()
 
             # THEN: The FileDialog.getExistingDirectory should have been called with the default caption
-            mocked_get_existing_directory.assert_called_once_with(self.widget, 'Select Directory', 'test/path/',
-                                                                  QtWidgets.QFileDialog.ShowDirsOnly)
+            mocked_get_existing_directory.assert_called_once_with(self.widget, 'Select Directory',
+                                                                  Path('test', 'path'),
+                                                                  PQFileDialog.ShowDirsOnly)
             self.assertFalse(mocked_get_open_file_name.called)
-            self.assertFalse(mocked_normpath.called)
 
     def test_on_browse_button_clicked_directory_custom_caption(self):
         """
@@ -149,45 +148,40 @@
         """
         # GIVEN: An instance of PathEdit with the `path_type` set to `Directories` and a mocked
         #        QFileDialog.getExistingDirectory with `default_caption` set.
-        with patch('openlp.core.ui.lib.pathedit.QtWidgets.QFileDialog.getExistingDirectory', return_value='') as \
+        with patch('openlp.core.ui.lib.pathedit.PQFileDialog.getExistingDirectory', return_value=None) as \
                 mocked_get_existing_directory, \
-                patch('openlp.core.ui.lib.pathedit.QtWidgets.QFileDialog.getOpenFileName') as \
-                mocked_get_open_file_name, \
-                patch('openlp.core.ui.lib.pathedit.os.path.normpath') as mocked_normpath:
+                patch('openlp.core.ui.lib.pathedit.PQFileDialog.getOpenFileName') as mocked_get_open_file_name:
             self.widget._path_type = PathType.Directories
-            self.widget._path = 'test/path/'
+            self.widget._path = Path('test', 'path')
             self.widget.dialog_caption = 'Directory Caption'
 
             # WHEN: Calling on_browse_button_clicked
             self.widget.on_browse_button_clicked()
 
             # THEN: The FileDialog.getExistingDirectory should have been called with the custom caption
-            mocked_get_existing_directory.assert_called_once_with(self.widget, 'Directory Caption', 'test/path/',
-                                                                  QtWidgets.QFileDialog.ShowDirsOnly)
+            mocked_get_existing_directory.assert_called_once_with(self.widget, 'Directory Caption',
+                                                                  Path('test', 'path'),
+                                                                  PQFileDialog.ShowDirsOnly)
             self.assertFalse(mocked_get_open_file_name.called)
-            self.assertFalse(mocked_normpath.called)
 
     def test_on_browse_button_clicked_file(self):
         """
         Test the `browse_button` `clicked` handler on_browse_button_clicked when the `path_type` is set to Files.
         """
         # GIVEN: An instance of PathEdit with the `path_type` set to `Files` and a mocked QFileDialog.getOpenFileName
-        with patch('openlp.core.ui.lib.pathedit.QtWidgets.QFileDialog.getExistingDirectory') as \
-                mocked_get_existing_directory, \
-                patch('openlp.core.ui.lib.pathedit.QtWidgets.QFileDialog.getOpenFileName', return_value=('', '')) as \
-                mocked_get_open_file_name, \
-                patch('openlp.core.ui.lib.pathedit.os.path.normpath') as mocked_normpath:
+        with patch('openlp.core.ui.lib.pathedit.PQFileDialog.getExistingDirectory') as mocked_get_existing_directory, \
+                patch('openlp.core.ui.lib.pathedit.PQFileDialog.getOpenFileName', return_value=(None, '')) as \
+                mocked_get_open_file_name:
             self.widget._path_type = PathType.Files
-            self.widget._path = 'test/pat.h'
+            self.widget._path = Path('test', 'pat.h')
 
             # WHEN: Calling on_browse_button_clicked
             self.widget.on_browse_button_clicked()
 
             # THEN: The FileDialog.getOpenFileName should have been called with the default caption
-            mocked_get_open_file_name.assert_called_once_with(self.widget, 'Select File', 'test/pat.h',
+            mocked_get_open_file_name.assert_called_once_with(self.widget, 'Select File', Path('test', 'pat.h'),
                                                               self.widget.filters)
             self.assertFalse(mocked_get_existing_directory.called)
-            self.assertFalse(mocked_normpath.called)
 
     def test_on_browse_button_clicked_file_custom_caption(self):
         """
@@ -196,23 +190,20 @@
         """
         # GIVEN: An instance of PathEdit with the `path_type` set to `Files` and a mocked QFileDialog.getOpenFileName
         #        with `default_caption` set.
-        with patch('openlp.core.ui.lib.pathedit.QtWidgets.QFileDialog.getExistingDirectory') as \
-                mocked_get_existing_directory, \
-                patch('openlp.core.ui.lib.pathedit.QtWidgets.QFileDialog.getOpenFileName', return_value=('', '')) as \
-                mocked_get_open_file_name, \
-                patch('openlp.core.ui.lib.pathedit.os.path.normpath') as mocked_normpath:
+        with patch('openlp.core.ui.lib.pathedit.PQFileDialog.getExistingDirectory') as mocked_get_existing_directory, \
+                patch('openlp.core.ui.lib.pathedit.PQFileDialog.getOpenFileName', return_value=(None, '')) as \
+                mocked_get_open_file_name:
             self.widget._path_type = PathType.Files
-            self.widget._path = 'test/pat.h'
+            self.widget._path = Path('test', 'pat.h')
             self.widget.dialog_caption = 'File Caption'
 
             # WHEN: Calling on_browse_button_clicked
             self.widget.on_browse_button_clicked()
 
             # THEN: The FileDialog.getOpenFileName should have been called with the custom caption
-            mocked_get_open_file_name.assert_called_once_with(self.widget, 'File Caption', 'test/pat.h',
+            mocked_get_open_file_name.assert_called_once_with(self.widget, 'File Caption', Path('test', 'pat.h'),
                                                               self.widget.filters)
             self.assertFalse(mocked_get_existing_directory.called)
-            self.assertFalse(mocked_normpath.called)
 
     def test_on_browse_button_clicked_user_cancels(self):
         """
@@ -221,16 +212,14 @@
         """
         # GIVEN: An instance of PathEdit with a mocked QFileDialog.getOpenFileName which returns an empty str for the
         #        file path.
-        with patch('openlp.core.ui.lib.pathedit.QtWidgets.QFileDialog.getOpenFileName', return_value=('', '')) as \
-                mocked_get_open_file_name, \
-                patch('openlp.core.ui.lib.pathedit.os.path.normpath') as mocked_normpath:
+        with patch('openlp.core.ui.lib.pathedit.PQFileDialog.getOpenFileName', return_value=(None, '')) as \
+                mocked_get_open_file_name:
 
             # WHEN: Calling on_browse_button_clicked
             self.widget.on_browse_button_clicked()
 
             # THEN: normpath should not have been called
             self.assertTrue(mocked_get_open_file_name.called)
-            self.assertFalse(mocked_normpath.called)
 
     def test_on_browse_button_clicked_user_accepts(self):
         """
@@ -239,9 +228,8 @@
         """
         # GIVEN: An instance of PathEdit with a mocked QFileDialog.getOpenFileName which returns a str for the file
         #        path.
-        with patch('openlp.core.ui.lib.pathedit.QtWidgets.QFileDialog.getOpenFileName',
-                   return_value=('/test/pat.h', '')) as mocked_get_open_file_name, \
-                patch('openlp.core.ui.lib.pathedit.os.path.normpath') as mocked_normpath, \
+        with patch('openlp.core.ui.lib.pathedit.PQFileDialog.getOpenFileName',
+                   return_value=(Path('test', 'pat.h'), '')) as mocked_get_open_file_name, \
                 patch.object(self.widget, 'on_new_path'):
 
             # WHEN: Calling on_browse_button_clicked
@@ -249,7 +237,6 @@
 
             # THEN: normpath and `on_new_path` should have been called
             self.assertTrue(mocked_get_open_file_name.called)
-            mocked_normpath.assert_called_once_with('/test/pat.h')
             self.assertTrue(self.widget.on_new_path.called)
 
     def test_on_revert_button_clicked(self):
@@ -258,13 +245,13 @@
         """
         # GIVEN: An instance of PathEdit with a mocked `on_new_path`, and the `default_path` set.
         with patch.object(self.widget, 'on_new_path') as mocked_on_new_path:
-            self.widget.default_path = '/default/pat.h'
+            self.widget.default_path = Path('default', 'pat.h')
 
             # WHEN: Calling `on_revert_button_clicked`
             self.widget.on_revert_button_clicked()
 
             # THEN: on_new_path should have been called with the default path
-            mocked_on_new_path.assert_called_once_with('/default/pat.h')
+            mocked_on_new_path.assert_called_once_with(Path('default', 'pat.h'))
 
     def test_on_line_edit_editing_finished(self):
         """
@@ -272,13 +259,13 @@
         """
         # GIVEN: An instance of PathEdit with a mocked `line_edit` and `on_new_path`.
         with patch.object(self.widget, 'on_new_path') as mocked_on_new_path:
-            self.widget.line_edit = MagicMock(**{'text.return_value': '/test/pat.h'})
+            self.widget.line_edit = MagicMock(**{'text.return_value': 'test/pat.h'})
 
             # WHEN: Calling `on_line_edit_editing_finished`
             self.widget.on_line_edit_editing_finished()
 
             # THEN: on_new_path should have been called with the path enetered in `line_edit`
-            mocked_on_new_path.assert_called_once_with('/test/pat.h')
+            mocked_on_new_path.assert_called_once_with(Path('test', 'pat.h'))
 
     def test_on_new_path_no_change(self):
         """
@@ -286,11 +273,11 @@
         """
         # GIVEN: An instance of PathEdit with a test path and mocked `pathChanged` signal
         with patch('openlp.core.ui.lib.pathedit.PathEdit.path', new_callable=PropertyMock):
-            self.widget._path = '/old/test/pat.h'
+            self.widget._path = Path('/old', 'test', 'pat.h')
             self.widget.pathChanged = MagicMock()
 
             # WHEN: Calling `on_new_path` with the same path as the existing path
-            self.widget.on_new_path('/old/test/pat.h')
+            self.widget.on_new_path(Path('/old', 'test', 'pat.h'))
 
             # THEN: The `pathChanged` signal should not be emitted
             self.assertFalse(self.widget.pathChanged.emit.called)
@@ -301,11 +288,11 @@
         """
         # GIVEN: An instance of PathEdit with a test path and mocked `pathChanged` signal
         with patch('openlp.core.ui.lib.pathedit.PathEdit.path', new_callable=PropertyMock):
-            self.widget._path = '/old/test/pat.h'
+            self.widget._path = Path('/old', 'test', 'pat.h')
             self.widget.pathChanged = MagicMock()
 
             # WHEN: Calling `on_new_path` with the a new path
-            self.widget.on_new_path('/new/test/pat.h')
+            self.widget.on_new_path(Path('/new', 'test', 'pat.h'))
 
             # THEN: The `pathChanged` signal should be emitted
-            self.widget.pathChanged.emit.assert_called_once_with('/new/test/pat.h')
+            self.widget.pathChanged.emit.assert_called_once_with(Path('/new', 'test', 'pat.h'))


Follow ups