← Back to team overview

openlp-core team mailing list archive

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

 

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

Requested reviews:
  OpenLP Core (openlp-core)

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

Work in progress, not for merging
-- 
The attached diff has been truncated due to its size.
Your team OpenLP Core is requested to review the proposed merge of lp:~phill-ridout/openlp/pathlib into lp:openlp.
=== modified file 'openlp/core/__init__.py'
--- openlp/core/__init__.py	2017-05-30 18:42:35 +0000
+++ openlp/core/__init__.py	2017-06-19 07:33:18 +0000
@@ -33,6 +33,8 @@
 import shutil
 import sys
 import time
+from patches.shutilpatches import copytree
+from pathlib2 import Path
 from traceback import format_exception
 
 from PyQt5 import QtCore, QtGui, QtWidgets
@@ -182,8 +184,8 @@
         Check if the data folder path exists.
         """
         data_folder_path = AppLocation.get_data_path()
-        if not os.path.exists(data_folder_path):
-            log.critical('Database was not found in: ' + data_folder_path)
+        if not data_folder_path.exists():
+            log.critical('Database was not found in: {data_folder_path}'.format(data_folder_path=data_folder_path))
             status = QtWidgets.QMessageBox.critical(None, translate('OpenLP', 'Data Directory Error'),
                                                     translate('OpenLP', 'OpenLP data folder was not found in:\n\n{path}'
                                                                         '\n\nThe location of the data folder was '
@@ -255,9 +257,9 @@
                 # Create copy of data folder
                 data_folder_path = AppLocation.get_data_path()
                 timestamp = time.strftime("%Y%m%d-%H%M%S")
-                data_folder_backup_path = data_folder_path + '-' + timestamp
+                data_folder_backup_path = data_folder_path.with_name(data_folder_path.name + '-' + timestamp)
                 try:
-                    shutil.copytree(data_folder_path, data_folder_backup_path)
+                    copytree(data_folder_path, data_folder_backup_path)
                 except OSError:
                     QtWidgets.QMessageBox.warning(None, translate('OpenLP', 'Backup'),
                                                   translate('OpenLP', 'Backup of the data folder failed!'))
@@ -346,15 +348,20 @@
     """
     Setup our logging using log_path
 
-    :param log_path: the path
+    :param log_path: The file to save the log to
+    :type log_path: Path
+
+    :return: None
+    :rtype: None
     """
     check_directory_exists(log_path, True)
-    filename = os.path.join(log_path, 'openlp.log')
-    logfile = logging.FileHandler(filename, 'w', encoding="UTF-8")
+    file_path = log_path / 'openlp.log'
+    # TODO: FileHandler accepts a Path object in Py3.6
+    logfile = logging.FileHandler(str(file_path), 'w', encoding='UTF-8')
     logfile.setFormatter(logging.Formatter('%(asctime)s %(name)-55s %(levelname)-8s %(message)s'))
     log.addHandler(logfile)
     if log.isEnabledFor(logging.DEBUG):
-        print('Logging to: {name}'.format(name=filename))
+        print('Logging to: {name}'.format(name=file_path))
 
 
 def main(args=None):
@@ -390,19 +397,19 @@
         application.setApplicationName('OpenLPPortable')
         Settings.setDefaultFormat(Settings.IniFormat)
         # Get location OpenLPPortable.ini
-        application_path = AppLocation.get_directory(AppLocation.AppDir)
-        set_up_logging(os.path.abspath(os.path.join(application_path, '..', '..', 'Other')))
+        portable_path = (AppLocation.get_directory(AppLocation.AppDir) / '..' / '..').resolve()
+        set_up_logging(portable_path / 'Other')
         log.info('Running portable')
-        portable_settings_file = os.path.abspath(os.path.join(application_path, '..', '..', 'Data', 'OpenLP.ini'))
+        portable_settings_path = portable_path / 'Data' / 'OpenLP.ini'
         # Make this our settings file
-        log.info('INI file: {name}'.format(name=portable_settings_file))
-        Settings.set_filename(portable_settings_file)
+        log.info('INI file: {name}'.format(name=portable_settings_path))
+        Settings.set_filename(portable_settings_path)
         portable_settings = Settings()
         # Set our data path
-        data_path = os.path.abspath(os.path.join(application_path, '..', '..', 'Data',))
+        data_path = portable_path / 'Data'
         log.info('Data path: {name}'.format(name=data_path))
         # Point to our data path
-        portable_settings.setValue('advanced/data path', data_path)
+        portable_settings.set_path_value('advanced/data path', data_path)
         portable_settings.setValue('advanced/is portable', True)
         portable_settings.sync()
     else:

=== modified file 'openlp/core/common/__init__.py'
--- openlp/core/common/__init__.py	2017-05-24 20:01:46 +0000
+++ openlp/core/common/__init__.py	2017-06-19 07:33:18 +0000
@@ -32,7 +32,7 @@
 import traceback
 from chardet.universaldetector import UniversalDetector
 from ipaddress import IPv4Address, IPv6Address, AddressValueError
-from pathlib import Path
+from pathlib2 import Path
 from shutil import which
 from subprocess import check_output, CalledProcessError, STDOUT
 
@@ -65,17 +65,23 @@
 
 def check_directory_exists(directory, do_not_log=False):
     """
-    Check a theme directory exists and if not create it
+    Check a directory exists and if not create it
 
     :param directory: The directory to make sure exists
+    :type directory: Path
+    
     :param do_not_log: To not log anything. This is need for the start up, when the log isn't ready.
+    :type do_not_log: bool
+    
+    :return: None
+    :rtype: None
     """
     if not do_not_log:
         log.debug('check_directory_exists {text}'.format(text=directory))
     try:
-        if not os.path.exists(directory):
-            os.makedirs(directory)
-    except IOError as e:
+        if not directory.exists():
+            directory.mkdir(parents=True)
+    except IOError:
         if not do_not_log:
             log.exception('failed to check if directory exists or create directory')
 
@@ -95,7 +101,7 @@
     :return: None
     :rtype: None
     """
-    app_dir = Path(AppLocation.get_directory(AppLocation.AppDir)).parent
+    app_dir = AppLocation.get_directory(AppLocation.AppDir).parent
     for extension_path in app_dir.glob(glob_pattern):
         extension_path = extension_path.relative_to(app_dir)
         if extension_path.name in excluded_files:
@@ -127,7 +133,7 @@
     """
     Return a path based on the system status.
 
-    :param frozen_option:
+    :param frozen_option: 
     :param non_frozen_option:
     """
     if hasattr(sys, 'frozen') and sys.frozen == 1:
@@ -378,20 +384,24 @@
         return os.path.split(path)
 
 
-def delete_file(file_path_name):
+def delete_file(file_path):
     """
     Deletes a file from the system.
 
-    :param file_path_name: The file, including path, to delete.
+    :param file_path: The file, including path, to delete.
+    :type file_path: Path
+    
+    :return: True if the deletion was successful, or the file never existed. False otherwise.
+    :rtype: bool
     """
-    if not file_path_name:
+    if not file_path:
         return False
     try:
-        if os.path.exists(file_path_name):
-            os.remove(file_path_name)
+        if file_path.exists():
+            file_path.unlink()
         return True
     except (IOError, OSError):
-        log.exception("Unable to delete file {text}".format(text=file_path_name))
+        log.exception("Unable to delete file {text}".format(text=file_path))
         return False
 
 
@@ -411,18 +421,21 @@
     return IMAGES_FILTER
 
 
-def is_not_image_file(file_name):
+def is_not_image_file(file_path):
     """
     Validate that the file is not an image file.
 
-    :param file_name: File name to be checked.
+    :param file_path: The file to be checked.
+    :type file_path: Path
+    
+    :return: If the file is not an image
+    :rtype: bool
     """
-    if not file_name:
+    if not file_path and not file_path.exists():
         return True
     else:
         formats = [bytes(fmt).decode().lower() for fmt in QtGui.QImageReader.supportedImageFormats()]
-        file_part, file_extension = os.path.splitext(str(file_name))
-        if file_extension[1:].lower() in formats and os.path.exists(file_name):
+        if file_path.suffix[1:].lower() in formats:
             return False
         return True
 
@@ -432,9 +445,11 @@
     Removes invalid characters from the given ``filename``.
 
     :param filename:  The "dirty" file name to clean.
+    :type filename: str
+    
+    :return: The cleaned string
+    :rtype: str
     """
-    if not isinstance(filename, str):
-        filename = str(filename, 'utf-8')
     return INVALID_FILE_CHARS.sub('_', CONTROL_CHARS.sub('', filename))
 
 
@@ -443,7 +458,10 @@
     Function that checks whether a binary exists.
 
     :param program_path: The full path to the binary to check.
+    :type program_path: Path
+    
     :return: program output to be parsed
+    :rtype: bytes
     """
     log.debug('testing program_path: {text}'.format(text=program_path))
     try:
@@ -453,26 +471,29 @@
             startupinfo.dwFlags |= STARTF_USESHOWWINDOW
         else:
             startupinfo = None
-        runlog = check_output([program_path, '--help'], stderr=STDOUT, startupinfo=startupinfo)
+        run_log = check_output([str(program_path), '--help'], stderr=STDOUT, startupinfo=startupinfo)
     except CalledProcessError as e:
-        runlog = e.output
+        run_log = e.output
     except Exception:
         trace_error_handler(log)
-        runlog = ''
-    log.debug('check_output returned: {text}'.format(text=runlog))
-    return runlog
-
-
-def get_file_encoding(filename):
+        run_log = ''
+    log.debug('check_output returned: {text}'.format(text=run_log))
+    return run_log
+
+
+def get_file_encoding(file_path):
     """
     Utility function to incrementally detect the file encoding.
 
-    :param filename: Filename for the file to determine the encoding for. Str
+    :param file_path: Filename for the file to determine the encoding for.
+    :type file_path: Path
+
     :return: A dict with the keys 'encoding' and 'confidence'
+    :rtype: dict[str, float]
     """
     detector = UniversalDetector()
     try:
-        with open(filename, 'rb') as detect_file:
+        with file_path.open('rb') as detect_file:
             while not detector.done:
                 chunk = detect_file.read(1024)
                 if not chunk:

=== modified file 'openlp/core/common/applocation.py'
--- openlp/core/common/applocation.py	2016-12-31 11:01:36 +0000
+++ openlp/core/common/applocation.py	2017-06-19 07:33:18 +0000
@@ -25,6 +25,7 @@
 import logging
 import os
 import sys
+from pathlib2 import Path
 
 from openlp.core.common import Settings, is_win, is_macosx
 
@@ -42,6 +43,9 @@
 
 log = logging.getLogger(__name__)
 
+FROZEN_APP_PATH = Path(sys.argv[0]).parent
+APP_PATH = Path(openlp.__file__).parent
+
 
 class AppLocation(object):
     """
@@ -63,20 +67,19 @@
         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: Path
         """
-        if dir_type == AppLocation.AppDir:
-            return get_frozen_path(os.path.abspath(os.path.dirname(sys.argv[0])), os.path.dirname(openlp.__file__))
+        if dir_type == AppLocation.AppDir or dir_type == AppLocation.VersionDir:
+            return get_frozen_path(FROZEN_APP_PATH, APP_PATH)
         elif dir_type == AppLocation.PluginsDir:
-            app_path = os.path.abspath(os.path.dirname(sys.argv[0]))
-            return get_frozen_path(os.path.join(app_path, 'plugins'),
-                                   os.path.join(os.path.dirname(openlp.__file__), 'plugins'))
-        elif dir_type == AppLocation.VersionDir:
-            return get_frozen_path(os.path.abspath(os.path.dirname(sys.argv[0])), os.path.dirname(openlp.__file__))
+            return get_frozen_path(FROZEN_APP_PATH, APP_PATH) / 'plugins'
         elif dir_type == AppLocation.LanguageDir:
-            app_path = get_frozen_path(os.path.abspath(os.path.dirname(sys.argv[0])), _get_os_dir_path(dir_type))
-            return os.path.join(app_path, 'i18n')
+            return get_frozen_path(FROZEN_APP_PATH, _get_os_dir_path(dir_type)) / 'i18n'
         elif dir_type == AppLocation.DataDir and AppLocation.BaseDir:
-            return os.path.join(AppLocation.BaseDir, 'data')
+            return Path(AppLocation.BaseDir, 'data')
         else:
             return _get_os_dir_path(dir_type)
 
@@ -84,47 +87,50 @@
     def get_data_path():
         """
         Return the path OpenLP stores all its data under.
+        
+        :return: The data path to use.
+        :rtype: Path
         """
         # Check if we have a different data location.
         if Settings().contains('advanced/data path'):
-            path = Settings().value('advanced/data path')
+            path = Settings().path_value('advanced/data path')
         else:
             path = AppLocation.get_directory(AppLocation.DataDir)
             check_directory_exists(path)
-        return os.path.normpath(path)
+        return path
 
     @staticmethod
-    def get_files(section=None, extension=None):
+    def get_files(section=None, extension=''):
         """
         Get a list of files from the data files path.
-
-        :param section: Defaults to *None*. The section of code getting the files - used to load from a section's
-            data subdirectory.
-        :param extension:
-            Defaults to *None*. The extension to search for. For example::
-
-                '.png'
+        
+        :param section: Defaults to *None*. The section of code getting the files - used to load from a section's data 
+        subdirectory.
+        :type section: None or str
+
+        :param extension: Defaults to ''. The extension to search for. For example::
+
+            '.png'
+        :type extension: str
+        
+        :return: List of files found.
+        :rtype: list of str
         """
         path = AppLocation.get_data_path()
         if section:
-            path = os.path.join(path, section)
+            path = path / section
         try:
-            files = os.listdir(path)
+            files = path.glob('*' + extension)
+            return [file.relative_to(path) for file in files]  # TODO: Could/should this be an iterator?
         except OSError:
             return []
-        if extension:
-            return [filename for filename in files if extension == os.path.splitext(filename)[1]]
-        else:
-            # no filtering required
-            return files
 
     @staticmethod
     def get_section_data_path(section):
         """
         Return the path a particular module stores its data under.
         """
-        data_path = AppLocation.get_data_path()
-        path = os.path.join(data_path, section)
+        path = AppLocation.get_data_path() / section
         check_directory_exists(path)
         return path
 
@@ -132,36 +138,43 @@
 def _get_os_dir_path(dir_type):
     """
     Return a path based on which OS and environment we are running in.
+    
+    :param dir_type: AppLocation Enum of the requested path type
+    :type dir_type: AppLocation Enum
+    
+    :return: The requested path
+    :rtype: Path
     """
     # If running from source, return the language directory from the source directory
     if dir_type == AppLocation.LanguageDir:
-        directory = os.path.abspath(os.path.join(os.path.dirname(openlp.__file__), '..', 'resources'))
-        if os.path.exists(directory):
+        directory = Path(os.path.abspath(os.path.join(os.path.dirname(openlp.__file__), '..', 'resources')))
+        if directory.exists():
             return directory
     if is_win():
+        openlp_folder_path = Path(os.getenv('APPDATA'), 'openlp')
         if dir_type == AppLocation.DataDir:
-            return os.path.join(str(os.getenv('APPDATA')), 'openlp', 'data')
+            return openlp_folder_path / 'data'
         elif dir_type == AppLocation.LanguageDir:
             return os.path.dirname(openlp.__file__)
-        return os.path.join(str(os.getenv('APPDATA')), 'openlp')
+        return openlp_folder_path
     elif is_macosx():
+        openlp_folder_path = Path(os.getenv('HOME'), 'Library', 'Application Support', 'openlp')
         if dir_type == AppLocation.DataDir:
-            return os.path.join(str(os.getenv('HOME')), 'Library', 'Application Support', 'openlp', 'Data')
+            return openlp_folder_path / 'Data'
         elif dir_type == AppLocation.LanguageDir:
             return os.path.dirname(openlp.__file__)
-        return os.path.join(str(os.getenv('HOME')), 'Library', 'Application Support', 'openlp')
+        return openlp_folder_path
     else:
         if dir_type == AppLocation.LanguageDir:
-            for prefix in ['/usr/local', '/usr']:
-                directory = os.path.join(prefix, 'share', 'openlp')
-                if os.path.exists(directory):
-                    return directory
-            return os.path.join('/usr', 'share', 'openlp')
+            directory = Path('/usr', 'local', 'share', 'openlp')
+            if directory.exists():
+                return directory
+            return Path('/usr', 'share', 'openlp')
         if XDG_BASE_AVAILABLE:
-            if dir_type == AppLocation.DataDir:
-                return os.path.join(str(BaseDirectory.xdg_data_home), 'openlp')
+            if dir_type == AppLocation.DataDir or dir_type == AppLocation.CacheDir:
+                return Path(BaseDirectory.xdg_data_home, 'openlp')
             elif dir_type == AppLocation.CacheDir:
-                return os.path.join(str(BaseDirectory.xdg_cache_home), 'openlp')
+                return Path(BaseDirectory.xdg_cache_home, 'openlp')
         if dir_type == AppLocation.DataDir:
-            return os.path.join(str(os.getenv('HOME')), '.openlp', 'data')
-        return os.path.join(str(os.getenv('HOME')), '.openlp')
+            return Path(os.getenv('HOME'), '.openlp', 'data')
+        return Path(os.getenv('HOME'), '.openlp')

=== modified file 'openlp/core/common/httputils.py'
--- openlp/core/common/httputils.py	2017-02-26 21:14:49 +0000
+++ openlp/core/common/httputils.py	2017-06-19 07:33:18 +0000
@@ -32,6 +32,7 @@
 import urllib.parse
 import urllib.request
 from http.client import HTTPException
+from pathlib2 import Path
 from random import randint
 
 from openlp.core.common import Registry, trace_error_handler
@@ -210,6 +211,7 @@
     :param callback: the class which needs to be updated
     :param url: URL to download
     :param f_path: Destination file
+    :type f_path: Path
     :param sha256: The check sum value to be checked against the download value
     """
     block_count = 0
@@ -217,29 +219,28 @@
     retries = 0
     while True:
         try:
-            filename = open(f_path, "wb")
-            url_file = urllib.request.urlopen(url, timeout=CONNECTION_TIMEOUT)
-            if sha256:
-                hasher = hashlib.sha256()
-            # Download until finished or canceled.
-            while not callback.was_cancelled:
-                data = url_file.read(block_size)
-                if not data:
-                    break
-                filename.write(data)
+            with f_path.open('wb') as filename:
+                url_file = urllib.request.urlopen(url, timeout=CONNECTION_TIMEOUT)
                 if sha256:
-                    hasher.update(data)
-                block_count += 1
-                callback._download_progress(block_count, block_size)
-            filename.close()
-            if sha256 and hasher.hexdigest() != sha256:
-                log.error('sha256 sums did not match for file: {file}'.format(file=f_path))
-                os.remove(f_path)
-                return False
-        except (urllib.error.URLError, socket.timeout) as err:
+                    hasher = hashlib.sha256()
+                # Download until finished or canceled.
+                while not callback.was_cancelled:
+                    data = url_file.read(block_size)
+                    if not data:
+                        break
+                    filename.write(data)
+                    if sha256:
+                        hasher.update(data)
+                    block_count += 1
+                    callback._download_progress(block_count, block_size)
+                filename.close()
+                if sha256 and hasher.hexdigest() != sha256:
+                    log.error('sha256 sums did not match for file: {file}'.format(file=f_path))
+                    f_path.unlink()
+                    return False
+        except (urllib.error.URLError, socket.timeout):
             trace_error_handler(log)
-            filename.close()
-            os.remove(f_path)
+            f_path.unlink()
             if retries > CONNECTION_RETRIES:
                 return False
             else:
@@ -249,7 +250,7 @@
         break
     # Delete file if cancelled, it may be a partial file.
     if callback.was_cancelled:
-        os.remove(f_path)
+        f_path.unlink()
     return True
 
 

=== modified file 'openlp/core/common/languagemanager.py'
--- openlp/core/common/languagemanager.py	2017-05-30 18:42:35 +0000
+++ openlp/core/common/languagemanager.py	2017-06-19 07:33:18 +0000
@@ -53,7 +53,7 @@
         """
         if LanguageManager.auto_language:
             language = QtCore.QLocale.system().name()
-        lang_path = AppLocation.get_directory(AppLocation.LanguageDir)
+        lang_path = str(AppLocation.get_directory(AppLocation.LanguageDir))
         app_translator = QtCore.QTranslator()
         app_translator.load(language, lang_path)
         # A translator for buttons and other default strings provided by Qt.
@@ -72,7 +72,7 @@
         Find all available language files in this OpenLP install
         """
         log.debug('Translation files: {files}'.format(files=AppLocation.get_directory(AppLocation.LanguageDir)))
-        trans_dir = QtCore.QDir(AppLocation.get_directory(AppLocation.LanguageDir))
+        trans_dir = QtCore.QDir(str(AppLocation.get_directory(AppLocation.LanguageDir)))
         file_names = trans_dir.entryList(['*.qm'], QtCore.QDir.Files, QtCore.QDir.Name)
         # Remove qm files from the list which start with "qt".
         file_names = [file_ for file_ in file_names if not file_.startswith('qt')]
@@ -141,7 +141,7 @@
             if reg_ex.exactMatch(qmf):
                 name = '{regex}'.format(regex=reg_ex.cap(1))
                 LanguageManager.__qm_list__[
-                    '{count:>2i} {name}'.format(count=counter + 1, name=LanguageManager.language_name(qmf))] = name
+                    '{count:>2} {name}'.format(count=counter + 1, name=LanguageManager.language_name(qmf))] = name
 
     @staticmethod
     def get_qm_list():

=== modified file 'openlp/core/common/settings.py'
--- openlp/core/common/settings.py	2017-06-05 06:05:54 +0000
+++ openlp/core/common/settings.py	2017-06-19 07:33:18 +0000
@@ -26,6 +26,7 @@
 import logging
 import os
 
+from patches.utils import path_to_str, str_to_path
 from PyQt5 import QtCore, QtGui
 
 from openlp.core.common import ThemeLevel, SlideLimits, UiStrings, is_win, is_linux
@@ -175,6 +176,7 @@
         'SettingsImport/Make_Changes': 'At_Own_RISK',
         'SettingsImport/type': 'OpenLP_settings_export',
         'SettingsImport/version': '',
+        'songs/enable chords': True,
         'themes/global theme': '',
         'themes/last directory': '',
         'themes/last directory export': '',
@@ -236,13 +238,19 @@
         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 ini_path: ini file path
+        :type ini_path: Path
+
+        :return: None
+        :rtype: None
         """
-        Settings.__file_path__ = ini_file
+        Settings.__file_path__ = path_to_str(ini_path)
 
     @staticmethod
     def set_up_default_values():
@@ -411,6 +419,34 @@
             key = self.group() + '/' + key
         return Settings.__default_settings__[key]
 
+    def path_value(self, key):
+        """
+        Convert the value for the given ``key`` from a str to a Path object. The type of the default value must be str.
+
+        :param key: The key to return the value from.
+        :type key: str
+
+        :return: The value of the key as a Path object, or None if the value is an empty string
+        :rtype: Path or None
+        """
+        return str_to_path(self.value(key))
+
+
+    def set_path_value(self, key, value):
+        """
+        Convert the value for the a Path object to a str and store it under the given ``key``.
+
+        :param key: The key to return the value from.
+        :type key: str
+
+        :param value: The value to store under ``key``
+        :type value: Path or None
+
+        :return: None
+        :rtype: None
+        """
+        self.setValue(key, path_to_str(value))
+
     def remove_obsolete_settings(self):
         """
         This method is only called to clean up the config. It removes old settings and it renames settings. See

=== modified file 'openlp/core/common/versionchecker.py'
--- openlp/core/common/versionchecker.py	2016-07-01 21:17:20 +0000
+++ openlp/core/common/versionchecker.py	2017-06-19 07:33:18 +0000
@@ -95,7 +95,7 @@
             full_version = '{tag}-bzr{tree}'.format(tag=tag_version.strip(), tree=tree_revision.strip())
     else:
         # We're not running the development version, let's use the file.
-        file_path = AppLocation.get_directory(AppLocation.VersionDir)
+        file_path = str(AppLocation.get_directory(AppLocation.VersionDir))
         file_path = os.path.join(file_path, '.version')
         version_file = None
         try:

=== 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-06-19 07:33:18 +0000
@@ -25,11 +25,12 @@
 """
 import html
 import logging
+import math
 import os
 import re
-import math
+from pathlib2 import Path
 
-from PyQt5 import QtCore, QtGui, Qt, QtWidgets
+from PyQt5 import QtCore, QtGui, QtWidgets
 
 from openlp.core.common import translate
 
@@ -83,30 +84,30 @@
     Next = 3
 
 
-def get_text_file_string(text_file):
+def get_text_file_string(text_file_path):
     """
-    Open a file and return its content as unicode string. If the supplied file name is not a file then the function
+    Open a file and return its content as a string. If the supplied file path is not a file then the function
     returns False. If there is an error loading the file or the content can't be decoded then the function will return
     None.
 
-    :param text_file: The name of the file.
-    :return: The file as a single string
+    :param text_file_path: The path to the file.
+    :type text_file_path: Path
+    
+    :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 or False or None
     """
-    if not os.path.isfile(text_file):
+    if not text_file_path.is_file():
         return False
-    file_handle = None
     content = None
     try:
-        file_handle = open(text_file, 'r', encoding='utf-8')
-        if file_handle.read(3) != '\xEF\xBB\xBF':
-            # no BOM was found
-            file_handle.seek(0)
-        content = file_handle.read()
+        with text_file_path.open('r', encoding='utf-8') as file_handle:
+            if file_handle.read(3) != '\xEF\xBB\xBF':
+                # no BOM was found
+                file_handle.seek(0)
+            content = file_handle.read()
     except (IOError, UnicodeError):
-        log.exception('Failed to open text file {text}'.format(text=text_file))
-    finally:
-        if file_handle:
-            file_handle.close()
+        log.exception('Failed to open text file {text}'.format(text=text_file_path))
     return content
 
 
@@ -128,9 +129,12 @@
     QIcon instance, that icon is simply returned. If not, it builds a QIcon instance from the resource or file name.
 
     :param icon:
-        The icon to build. This can be a QIcon, a resource string in the form ``:/resource/file.png``, or a file
-        location like ``/path/to/file.png``. However, the **recommended** way is to specify a resource string.
+        The icon to build. This can be a QIcon, a resource string in the form ``:/resource/file.png``, or a file path
+        location like ``Path(/path/to/file.png)``. However, the **recommended** way is to specify a resource string.
+    :type icon: QtGui.QIcon or Path or QtGui.QIcon or str
+    
     :return: The build icon.
+    :rtype: QtGui.QIcon
     """
     if isinstance(icon, QtGui.QIcon):
         return icon
@@ -138,6 +142,8 @@
     button_icon = QtGui.QIcon()
     if isinstance(icon, str):
         pix_map = QtGui.QPixmap(icon)
+    elif isinstance(icon, Path):
+        pix_map = QtGui.QPixmap(str(icon))
     elif isinstance(icon, QtGui.QImage):
         pix_map = QtGui.QPixmap.fromImage(icon)
     if pix_map:
@@ -170,15 +176,20 @@
     """
     Create a thumbnail from the given image path and depending on ``return_icon`` it returns an icon from this thumb.
 
-    :param image_path: The image file to create the icon from.
-    :param thumb_path: The filename to save the thumbnail to.
+    :param image_path: The image path to create the icon from.
+    :type image_path: Path
+    
+    :param thumb_path: The path to save the thumbnail to.
+    :type thumb_path: Path
+    
     :param return_icon: States if an icon should be build and returned from the thumb. Defaults to ``True``.
     :param size: Allows to state a own size (QtCore.QSize) to use. Defaults to ``None``, which means that a default
      height of 88 is used.
+     
     :return: The final icon.
     """
-    ext = os.path.splitext(thumb_path)[1].lower()
-    reader = QtGui.QImageReader(image_path)
+    ext = thumb_path.suffix.upper()
+    reader = QtGui.QImageReader(str(image_path))
     if size is None:
         # No size given; use default height of 88
         if reader.size().isEmpty():
@@ -205,10 +216,10 @@
             # Invalid; use default height of 88
             reader.setScaledSize(QtCore.QSize(int(ratio * 88), 88))
     thumb = reader.read()
-    thumb.save(thumb_path, ext[1:])
+    thumb.save(str(thumb_path), ext[1:])
     if not return_icon:
         return
-    if os.path.exists(thumb_path):
+    if thumb_path.exists():
         return build_icon(thumb_path)
     # Fallback for files with animation support.
     return build_icon(image_path)
@@ -220,13 +231,18 @@
     before checking the existence of the file.
 
     :param file_path: The path to the file. The file **must** exist!
+    :type file_path: Path
+
     :param thumb_path: The path to the thumb.
-    :return: True, False if the image has changed since the thumb was created.
+    :type thumb_path: Path
+
+    :return: Has the image changed since the thumb was created?
+    :rtype: bool
     """
-    if not os.path.exists(thumb_path):
+    if not thumb_path.exists():
         return False
-    image_date = os.stat(file_path).st_mtime
-    thumb_date = os.stat(thumb_path).st_mtime
+    image_date = file_path.stat().st_mtime
+    thumb_date = thumb_path.stat().st_mtime
     return image_date <= thumb_date
 
 
@@ -237,12 +253,23 @@
     DO NOT REMOVE THE DEFAULT BACKGROUND VALUE!
 
     :param image_path: The path to the image to resize.
+    :type image_path: Path
+
     :param width: The new image width.
+    :type width: int
+
     :param height: The new image height.
-    :param background: The background colour. Defaults to black.
+    :type height: int
+    
+    :param background: The background colour. Defaults to black. See https://doc.qt.io/qt-5/qcolor.html#setNamedColor 
+                       for accepted input
+    :type background: str
+    
+    :return: A resized copy of the image
+    :rtype: QtGui.QImage
     """
     log.debug('resize_image - start')
-    reader = QtGui.QImageReader(image_path)
+    reader = QtGui.QImageReader(str(image_path))
     # The image's ratio.
     image_ratio = reader.size().width() / reader.size().height()
     resize_ratio = width / height

=== modified file 'openlp/core/lib/db.py'
--- openlp/core/lib/db.py	2017-05-27 18:21:24 +0000
+++ openlp/core/lib/db.py	2017-06-19 07:33:18 +0000
@@ -71,8 +71,7 @@
         return 'sqlite:///{path}/{plugin}.sqlite'.format(path=AppLocation.get_section_data_path(plugin_name),
                                                          plugin=plugin_name)
     else:
-        return 'sqlite:///{path}/{name}'.format(path=AppLocation.get_section_data_path(plugin_name),
-                                                name=db_file_name)
+        return 'sqlite:///{path}/{name}'.format(path=AppLocation.get_section_data_path(plugin_name), name=db_file_name)
 
 
 def handle_db_error(plugin_name, db_file_name):
@@ -205,10 +204,11 @@
     :param plugin_name: The name of the plugin to remove the database for
     :param db_file_name: The database file name. Defaults to None resulting in the plugin_name being used.
     """
+    db_file_path = AppLocation.get_section_data_path(plugin_name)
     if db_file_name:
-        db_file_path = os.path.join(AppLocation.get_section_data_path(plugin_name), db_file_name)
+        db_file_path = db_file_path / db_file_name
     else:
-        db_file_path = os.path.join(AppLocation.get_section_data_path(plugin_name), plugin_name)
+        db_file_path = db_file_path / plugin_name
     return delete_file(db_file_path)
 
 
@@ -223,9 +223,9 @@
 
         :param plugin_name:  The name to setup paths and settings section names
         :param init_schema: The init_schema function for this database
-        :param db_file_name: The upgrade_schema function for this database
-        :param upgrade_mod: The file name to use for this database. Defaults to None resulting in the plugin_name
+        :param db_file_name: The file name to use for this database. Defaults to None resulting in the plugin_name
         being used.
+        :param upgrade_mod: The upgrade_schema function for this database
         """
         self.is_dirty = False
         self.session = None

=== modified file 'openlp/core/lib/imagemanager.py'
--- openlp/core/lib/imagemanager.py	2017-05-30 18:42:35 +0000
+++ openlp/core/lib/imagemanager.py	2017-06-19 07:33:18 +0000
@@ -25,9 +25,9 @@
 wait for the conversion to happen.
 """
 import logging
-import os
 import time
 import queue
+from pathlib2 import Path
 
 from PyQt5 import QtCore
 
@@ -91,28 +91,39 @@
     Urgent = 0
 
 
-class Image(object):
+class Image:
     """
     This class represents an image. To mark an image as *dirty* call the :class:`ImageManager`'s ``_reset_image`` method
     with the Image instance as argument.
     """
     secondary_priority = 0
 
-    def __init__(self, path, source, background, width=-1, height=-1):
+    def __init__(self, image_path, source, background, width=-1, height=-1):
         """
         Create an image for the :class:`ImageManager`'s cache.
 
-        :param path: The image's file path. This should be an existing file path.
-        :param source: The source describes the image's origin. Possible values are described in the
-            :class:`~openlp.core.lib.ImageSource` class.
-        :param background: A ``QtGui.QColor`` object specifying the colour to be used to fill the gabs if the image's
-            ratio does not match with the display ratio.
+        :param image_path: The image's file path. This should be an existing file path.
+        :type: Path
+
+        :param source: The source describes the image's origin.
+        :type source: ImageSource
+
+        :param background: The colour to be used to fill the gaps if the image's ratio does not match with the display 
+                           ratio.
+        :type background: QtGui.QColor
+
         :param width: The width of the image, defaults to -1 meaning that the screen width will be used.
+        :type width: int
+
         :param height: The height of the image, defaults to -1 meaning that the screen height will be used.
+        :type height: int
+
+        :return: None
+        :rtype: None
         """
-        if not os.path.exists(path):
-            raise FileNotFoundError('{path} not found'.format(path=path))
-        self.path = path
+        if not image_path.exists():
+            raise FileNotFoundError('{path} not found'.format(path=image_path))
+        self.path = image_path
         self.image = None
         self.image_bytes = None
         self.priority = Priority.Normal
@@ -121,7 +132,7 @@
         self.timestamp = 0
         self.width = width
         self.height = height
-        self.timestamp = os.stat(path).st_mtime
+        self.timestamp = image_path.stat().st_mtime
         self.secondary_priority = Image.secondary_priority
         Image.secondary_priority += 1
 
@@ -205,15 +216,27 @@
                 image.background = background
                 self._reset_image(image)
 
-    def update_image_border(self, path, source, background, width=-1, height=-1):
+    def update_image_border(self, image_path, source, background_color, width=-1, height=-1):
         """
         Border has changed so update the image affected.
+
+        :param image_path: Path to the image file to update
+        :type image_path: Path
+
+        :param source: Source of the image
+        :type source: ImageSource
+
+        :param background_color: Background color of the image
+        :type: QtGui.QColor
+
+        :return: None
+        :rtype: None
         """
         log.debug('update_image_border')
         # Mark the image as dirty for a rebuild by setting the image and byte stream to None.
-        image = self._cache[(path, source, width, height)]
+        image = self._cache[(image_path, source, width, height)]
         if image.source == source:
-            image.background = background
+            image.background = background_color
             self._reset_image(image)
 
     def _reset_image(self, image):
@@ -232,12 +255,21 @@
         if not self.image_thread.isRunning():
             self.image_thread.start()
 
-    def get_image(self, path, source, width=-1, height=-1):
-        """
-        Return the ``QImage`` from the cache. If not present wait for the background thread to process it.
-        """
-        log.debug('getImage {path}'.format(path=path))
-        image = self._cache[(path, source, width, height)]
+    def get_image(self, image_path, source, width=-1, height=-1):
+        """
+        Return an image from the cache. If not present wait for the background thread to process it.
+
+        :param image_path: Path to the image file to get
+        :type image_path: Path
+        
+        :param source: Source of the image
+        :type source: ImageSource
+
+        :return: The cached image
+        :rtype: QtGui.QImage
+        """
+        log.debug('getImage {path}'.format(path=image_path))
+        image = self._cache[(image_path, source, width, height)]
         if image.image is None:
             self._conversion_queue.modify_priority(image, Priority.High)
             # make sure we are running and if not give it a kick
@@ -252,12 +284,21 @@
             self._conversion_queue.modify_priority(image, Priority.Low)
         return image.image
 
-    def get_image_bytes(self, path, source, width=-1, height=-1):
+    def get_image_bytes(self, image_path, source, width=-1, height=-1):
         """
         Returns the byte string for an image. If not present wait for the background thread to process it.
+        
+        :param image_path: Path to the image file to return
+        :type image_path: Path
+        
+        :param source: Source of the image
+        :type source: ImageSource
+
+        :return: The image encoded as bytes
+        :rtype: bytes
         """
-        log.debug('get_image_bytes {path}'.format(path=path))
-        image = self._cache[(path, source, width, height)]
+        log.debug('get_image_bytes {path}'.format(path=image_path))
+        image = self._cache[(image_path, source, width, height)]
         if image.image_bytes is None:
             self._conversion_queue.modify_priority(image, Priority.Urgent)
             # make sure we are running and if not give it a kick
@@ -267,20 +308,33 @@
                 time.sleep(0.1)
         return image.image_bytes
 
-    def add_image(self, path, source, background, width=-1, height=-1):
+    def add_image(self, image_path, source, background, width=-1, height=-1):
         """
         Add image to cache if it is not already there.
+        
+        :param image_path: Path to the image file to update
+        :type image_path: Path
+
+        :param source: Source of the image
+        :type source: ImageSource
+
+        :param background_color: Background color of the image
+        :type: QtGui.QColor
+
+        :return: None
+        :rtype: None
         """
-        log.debug('add_image {path}'.format(path=path))
-        if (path, source, width, height) not in self._cache:
-            image = Image(path, source, background, width, height)
-            self._cache[(path, source, width, height)] = image
+        log.debug('add_image {path}'.format(path=image_path))
+        if (image_path, source, width, height) not in self._cache:
+            image = Image(image_path, source, background, width, height)
+            self._cache[(image_path, source, width, height)] = image
             self._conversion_queue.put((image.priority, image.secondary_priority, image))
         # Check if the there are any images with the same path and check if the timestamp has changed.
         for image in list(self._cache.values()):
-            if os.path.exists(path):
-                if image.path == path and image.timestamp != os.stat(path).st_mtime:
-                    image.timestamp = os.stat(path).st_mtime
+            if image_path.exists():
+                # TODO: I'm sure i've seen this code in a function elsewhere refactor?
+                if image.path == image_path and image.timestamp != image_path.stat().st_mtime:
+                    image.timestamp = image_path.stat().st_mtime
                     self._reset_image(image)
         # We want only one thread.
         if not self.image_thread.isRunning():

=== modified file 'openlp/core/lib/mediamanageritem.py'
--- openlp/core/lib/mediamanageritem.py	2017-02-18 07:23:15 +0000
+++ openlp/core/lib/mediamanageritem.py	2017-06-19 07:33:18 +0000
@@ -23,10 +23,12 @@
 Provides the generic functions for interfacing plugins with the Media Manager.
 """
 import logging
-import os
 import re
+from pathlib2 import Path
 
-from PyQt5 import QtCore, QtGui, QtWidgets
+from patches.utils import path_to_str, str_to_path
+from patches.pyqt5patches import PQFileDialog
+from PyQt5 import QtCore, QtWidgets
 
 from openlp.core.common import Registry, RegistryProperties, Settings, UiStrings, translate
 from openlp.core.lib import FileDialog, ServiceItem, StringContent, ServiceItemContext
@@ -309,34 +311,37 @@
         """
         Add a file to the list widget to make it available for showing
         """
-        files = FileDialog.getOpenFileNames(self, self.on_new_prompt,
-                                            Settings().value(self.settings_section + '/last directory'),
-                                            self.on_new_file_masks)
-        log.info('New files(s) {files}'.format(files=files))
-        if files:
+        file_paths, filter_used = PQFileDialog.getOpenFileNames(
+            self, self.on_new_prompt,
+            Settings().path_value(self.settings_section + '/last directory'),
+            self.on_new_file_masks)
+        log.info('New files(s) {files}'.format(files=file_paths))
+        if file_paths:
             self.application.set_busy_cursor()
-            self.validate_and_load(files)
+            self.validate_and_load(file_paths)
         self.application.set_normal_cursor()
 
     def load_file(self, data):
         """
         Turn file from Drag and Drop into an array so the Validate code can run it.
+        
+        Registered by :mod:`openlp.core.ui.lib.listwidgetwithdnd` and :mod:`openlp.core.ui.lib.treewidgetwithdnd` 
 
         :param data: A dictionary containing the list of files to be loaded and the target
         """
         new_files = []
         error_shown = False
-        for file_name in data['files']:
-            file_type = file_name.split('.')[-1]
-            if file_type.lower() not in self.on_new_file_masks:
+        for file_path in data['file_paths']:
+            ext = file_path.suffix.lower()
+            if ext not in self.on_new_file_masks:
                 if not error_shown:
                     critical_error_message_box(translate('OpenLP.MediaManagerItem', 'Invalid File Type'),
                                                translate('OpenLP.MediaManagerItem',
                                                          'Invalid File {name}.\n'
-                                                         'Suffix not supported').format(name=file_name))
+                                                         'Suffix not supported').format(name=file_path))
                     error_shown = True
             else:
-                new_files.append(file_name)
+                new_files.append(file_path)
         if new_files:
             self.validate_and_load(new_files, data['target'])
 
@@ -348,33 +353,36 @@
         """
         pass
 
-    def validate_and_load(self, files, target_group=None):
+    def validate_and_load(self, file_paths, target_group=None):
         """
-        Process a list for files either from the File Dialog or from Drag and
-        Drop
-
-        :param files: The files to be loaded.
+        Process a list for files either from the File Dialog or from Drag and Drop
+
+        :param file_paths: The files to be loaded.
+        :type file_paths: list[Path]
+
         :param target_group: The QTreeWidgetItem of the group that will be the parent of the added files
         """
+        # TODO: To path object
         names = []
-        full_list = []
+        full_paths = []
         for count in range(self.list_view.count()):
             names.append(self.list_view.item(count).text())
-            full_list.append(self.list_view.item(count).data(QtCore.Qt.UserRole))
+            # TODO: Store Path objects in data
+            full_paths.append(self.list_view.item(count).data(QtCore.Qt.UserRole))
         duplicates_found = False
         files_added = False
-        for file_path in files:
-            if file_path in full_list:
+        for file_path in file_paths:
+            if file_path in full_paths:
                 duplicates_found = True
             else:
                 files_added = True
-                full_list.append(file_path)
-        if full_list and files_added:
+                full_paths.append(file_path)
+        if full_paths and files_added:
             if target_group is None:
                 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)
+            self.load_list(full_paths, target_group)
+            last_dir_path = file_paths[0].parent
+            Settings().set_path_value(self.settings_section + '/last directory', last_dir_path)
             Settings().setValue('{section}/{section} files'.format(section=self.settings_section),
                                 self.get_file_list())
         if duplicates_found:
@@ -396,14 +404,15 @@
 
     def get_file_list(self):
         """
-        Return the current list of files
+        :return: Return the current list of files
+        :rtype: list[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_path = list_item.data(QtCore.Qt.UserRole)
+            file_paths.append(file_path)
+        return file_paths
 
     def load_list(self, load_list, target_group):
         """

=== modified file 'openlp/core/lib/pluginmanager.py'
--- openlp/core/lib/pluginmanager.py	2017-05-15 10:09:59 +0000
+++ openlp/core/lib/pluginmanager.py	2017-06-19 07:33:18 +0000
@@ -40,7 +40,7 @@
         """
         super(PluginManager, self).__init__(parent)
         self.log_info('Plugin manager Initialising')
-        self.base_path = os.path.abspath(AppLocation.get_directory(AppLocation.PluginsDir))
+        self.base_path = os.path.abspath(str(AppLocation.get_directory(AppLocation.PluginsDir)))
         self.log_debug('Base path {path}'.format(path=self.base_path))
         self.plugins = []
         self.log_info('Plugin manager Initialised')

=== modified file 'openlp/core/lib/serviceitem.py'
--- openlp/core/lib/serviceitem.py	2017-05-17 20:06:45 +0000
+++ openlp/core/lib/serviceitem.py	2017-06-19 07:33:18 +0000
@@ -30,11 +30,13 @@
 import os
 import uuid
 import ntpath
+from pathlib2 import PureWindowsPath, Path
 
+from patches.utils import str_to_path
 from PyQt5 import QtGui
 
 from openlp.core.common import RegistryProperties, Settings, translate, AppLocation, md5_hash
-from openlp.core.lib import ImageSource, build_icon, clean_tags, expand_tags, expand_chords, create_thumb
+from openlp.core.lib import ImageSource, build_icon, clean_tags, expand_tags, expand_chords
 
 log = logging.getLogger(__name__)
 
@@ -226,6 +228,7 @@
 
         :param icon: A string to an icon in the resources or on disk.
         """
+        # TODO: Convert to Path object?
         self.icon = icon
         self.iconic_representation = build_icon(icon)
 
@@ -283,23 +286,26 @@
             self.raw_footer = []
         self.foot_text = '<br>'.join([_f for _f in self.raw_footer if _f])
 
-    def add_from_image(self, path, title, background=None, thumbnail=None):
+    def add_from_image(self, image_path, title, background=None, thumbnail=None):
         """
         Add an image slide to the service item.
 
-        :param path: The directory in which the image file is located.
+        :param image_path: The directory in which the image file is located.
+        :type image_path: Path
+
         :param title: A title for the slide in the service item.
         :param background:
         :param thumbnail: Optional alternative thumbnail, used for remote thumbnails.
         """
+        # TODO: Thumbnail as path object
         if background:
             self.image_border = background
         self.service_item_type = ServiceItemType.Image
         if not thumbnail:
-            self._raw_frames.append({'title': title, 'path': path})
+            self._raw_frames.append({'title': title, 'path': image_path})
         else:
-            self._raw_frames.append({'title': title, 'path': path, 'image': thumbnail})
-        self.image_manager.add_image(path, ImageSource.ImagePlugin, self.image_border)
+            self._raw_frames.append({'title': title, 'path': image_path, 'image': thumbnail})
+        self.image_manager.add_image(image_path, ImageSource.ImagePlugin, self.image_border)
         self._new_item()
 
     def add_from_text(self, raw_slide, verse_tag=None):
@@ -326,6 +332,8 @@
         :param display_title: Title to show in gui/webinterface, optional.
         :param notes: Notes to show in the webinteface, optional.
         """
+        # TODO: Accept Path object, image to path object?
+        path = Path(path)
         self.service_item_type = ServiceItemType.Command
         # If the item should have a display title but this frame doesn't have one, we make one up
         if self.is_capable(ItemCapabilities.HasDisplayTitle) and not display_title:
@@ -333,11 +341,12 @@
                                       '[slide {frame:d}]').format(frame=len(self._raw_frames) + 1)
         # Update image path to match servicemanager location if file was loaded from service
         if image and not self.has_original_files and self.name == 'presentations':
-            file_location = os.path.join(path, file_name)
-            file_location_hash = md5_hash(file_location.encode('utf-8'))
-            image = os.path.join(AppLocation.get_section_data_path(self.name), 'thumbnails',
-                                 file_location_hash, ntpath.basename(image))
-        self._raw_frames.append({'title': file_name, 'image': image, 'path': path,
+            file_location = Path(path, file_name)
+            file_location_hash = md5_hash(bytes(file_location))
+            # TODO: Can ntpath be replaced by the Path object?
+            image = AppLocation.get_section_data_path(self.name) / 'thumbnails' / file_location_hash / ntpath.basename(image)
+        # TODO: To path object
+        self._raw_frames.append({'title': file_name, 'image': image, 'path': Path(path),
                                  'display_title': display_title, 'notes': notes})
         if self.is_capable(ItemCapabilities.HasThumbnails):
             self.image_manager.add_image(image, ImageSource.CommandPlugins, '#000000')
@@ -388,16 +397,17 @@
                                      'display_title': slide['display_title'], 'notes': slide['notes']})
         return {'header': service_header, 'data': service_data}
 
-    def set_from_service(self, service_item, path=None):
+    def set_from_service(self, service_item, service_path=None):
         """
         This method takes a service item from a saved service file (passed from the ServiceManager) and extracts the
         data actually required.
 
         :param service_item: The item to extract data from.
-        :param path: Defaults to *None*. This is the service manager path for things which have their files saved
+        :param service_path: Defaults to *None*. This is the service manager path for things which have their files saved
             with them or None when the saved service is lite and the original file paths need to be preserved.
         """
-        log.debug('set_from_service called with path {path}'.format(path=path))
+        # TODO: accept Path object
+        log.debug('set_from_service called with service_path {path}'.format(path=service_path))
         header = service_item['serviceitem']['header']
         self.title = header['title']
         self.name = header['name']
@@ -426,11 +436,13 @@
             self.background_audio = []
             for filename in header['background_audio']:
                 # Give them real file paths.
-                filepath = filename
-                if path:
+                # TODO: To path object
+                file_path = Path(filename)
+                if service_path:
                     # Windows can handle both forward and backward slashes, so we use ntpath to get the basename
-                    filepath = os.path.join(path, ntpath.basename(filename))
-                self.background_audio.append(filepath)
+                    file_path = Path(service_path, ntpath.basename(filename))
+                # TODO: To path object
+                self.background_audio.append(str(file_path))
         self.theme_overwritten = header.get('theme_overwritten', False)
         if self.service_item_type == ServiceItemType.Text:
             for slide in service_item['serviceitem']['data']:
@@ -438,14 +450,16 @@
         elif self.service_item_type == ServiceItemType.Image:
             settings_section = service_item['serviceitem']['header']['name']
             background = QtGui.QColor(Settings().value(settings_section + '/background color'))
-            if path:
+            if service_path:
                 self.has_original_files = False
                 for text_image in service_item['serviceitem']['data']:
-                    filename = os.path.join(path, text_image)
-                    self.add_from_image(filename, text_image, background)
+                    # TODO: service_path to Path object
+                    image_path = Path(service_path, text_image)
+                    self.add_from_image(image_path, text_image, background)
             else:
                 for text_image in service_item['serviceitem']['data']:
-                    self.add_from_image(text_image['path'], text_image['title'], background)
+                    # TODO: service_path to Path object
+                    self.add_from_image(Path(text_image['path']), text_image['title'], background)
         elif self.service_item_type == ServiceItemType.Command:
             for text_image in service_item['serviceitem']['data']:
                 if not self.title:
@@ -453,12 +467,13 @@
                 if self.is_capable(ItemCapabilities.IsOptical):
                     self.has_original_files = False
                     self.add_from_command(text_image['path'], text_image['title'], text_image['image'])
-                elif path:
+                elif service_path:
                     self.has_original_files = False
-                    self.add_from_command(path, text_image['title'], text_image['image'],
+                    # TODO: To path object
+                    self.add_from_command(str(service_path), text_image['title'], text_image['image'],
                                           text_image.get('display_title', ''), text_image.get('notes', ''))
                 else:
-                    self.add_from_command(text_image['path'], text_image['title'], text_image['image'])
+                    self.add_from_command(text_image['service_path'], text_image['title'], text_image['image'])
         self._new_item()
 
     def get_display_title(self):
@@ -535,7 +550,7 @@
         Confirms if the ServiceItem uses a file
         """
         return self.service_item_type == ServiceItemType.Image or \
-            (self.service_item_type == ServiceItemType.Command and not self.is_capable(ItemCapabilities.IsOptical))
+               (self.service_item_type == ServiceItemType.Command and not self.is_capable(ItemCapabilities.IsOptical))
 
     def is_text(self):
         """
@@ -587,6 +602,9 @@
     def get_frame_path(self, row=0, frame=None):
         """
         Returns the path of the raw frame
+
+        :return: Path to the raw frame
+        :rtype: Path
         """
         if not frame:
             try:
@@ -594,10 +612,10 @@
             except IndexError:
                 return ''
         if self.is_image() or self.is_capable(ItemCapabilities.IsOptical):
-            path_from = frame['path']
+            frame_path = frame['path']
         else:
-            path_from = os.path.join(frame['path'], frame['title'])
-        return path_from
+            frame_path = frame['path'] / frame['title']
+        return frame_path
 
     def remove_frame(self, frame):
         """
@@ -646,7 +664,7 @@
         """
         if self.uses_file():
             for frame in self.get_frames():
-                if self.get_frame_path(frame=frame) in invalid_paths:
+                if str(self.get_frame_path(frame=frame)) in invalid_paths:
                     self.remove_frame(frame)
 
     def missing_frames(self):
@@ -661,17 +679,19 @@
         """
         self.is_valid = True
         for frame in self._raw_frames:
-            if self.is_image() and not os.path.exists(frame['path']):
+            # TODO: To Path object
+            if self.is_image() and not Path(frame['path']).exists():
                 self.is_valid = False
                 break
             elif self.is_command():
                 if self.is_capable(ItemCapabilities.IsOptical):
+                    # TODO: path object
                     if not os.path.exists(frame['title']):
                         self.is_valid = False
                         break
                 else:
-                    file_name = os.path.join(frame['path'], frame['title'])
-                    if not os.path.exists(file_name):
+                    file_path = frame['path'] / frame['title']
+                    if not file_path.exists():
                         self.is_valid = False
                         break
                     if suffix_list and not self.is_text():

=== modified file 'openlp/core/lib/theme.py'
--- openlp/core/lib/theme.py	2017-05-24 19:55:30 +0000
+++ openlp/core/lib/theme.py	2017-06-19 07:33:18 +0000
@@ -22,9 +22,10 @@
 """
 Provide the theme XML and handling functions for OpenLP v2 themes.
 """
+import json
+import logging
 import os
-import logging
-import json
+from pathlib2 import Path
 
 from lxml import etree, objectify
 from openlp.core.common import AppLocation, de_hump
@@ -158,12 +159,11 @@
         Initialise the theme object.
         """
         # basic theme object with defaults
-        json_dir = os.path.join(AppLocation.get_directory(AppLocation.AppDir), 'core', 'lib', 'json')
-        json_file = os.path.join(json_dir, 'theme.json')
+        json_file = AppLocation.get_directory(AppLocation.AppDir) / 'core' / 'lib' / 'json' / 'theme.json'
         jsn = get_text_file_string(json_file)
         jsn = json.loads(jsn)
         self.expand_json(jsn)
-        self.background_filename = ''
+        self.background_filename = None
 
     def expand_json(self, var, prev=None):
         """
@@ -187,12 +187,15 @@
         Add the path name to the image name so the background can be rendered.
 
         :param path: The path name to be added.
+        :type path: Path
+        
+        :return: None
+        :rtype: None
         """
         if self.background_type == 'image' or self.background_type == 'video':
             if self.background_filename and path:
                 self.theme_name = self.theme_name.strip()
-                self.background_filename = self.background_filename.strip()
-                self.background_filename = os.path.join(path, self.theme_name, self.background_filename)
+                self.background_filename = path / self.theme_name / self.background_filename
 
     def set_default_header_footer(self):
         """

=== modified file 'openlp/core/ui/advancedtab.py'
--- openlp/core/ui/advancedtab.py	2017-05-30 18:50:39 +0000
+++ openlp/core/ui/advancedtab.py	2017-06-19 07:33:18 +0000
@@ -22,9 +22,9 @@
 """
 The :mod:`advancedtab` provides an advanced settings facility.
 """
+import logging
 from datetime import datetime, timedelta
-import logging
-import os
+from pathlib2 import Path
 
 from PyQt5 import QtCore, QtGui, QtWidgets
 
@@ -485,24 +485,30 @@
         self.service_name_edit.setText(UiStrings().DefaultServiceName)
         self.service_name_edit.setFocus()
 
-    def on_data_directory_path_edit_path_changed(self, new_data_path):
+    def on_data_directory_path_edit_path_changed(self, new_path):
         """
-        Browse for a new data directory location.
+        Handle the `editPathChanged` signal of the data_directory_path_edit
+        
+        :param new_path: The new path
+        :type new_path: Path
+        
+        :return: None
+        :rtype: None
         """
         # Make sure they want to change the data.
         answer = QtWidgets.QMessageBox.question(self, translate('OpenLP.AdvancedTab', 'Confirm Data Directory Change'),
                                                 translate('OpenLP.AdvancedTab', 'Are you sure you want to change the '
                                                           'location of the OpenLP data directory to:\n\n{path}'
                                                           '\n\nThe data directory will be changed when OpenLP is '
-                                                          'closed.').format(path=new_data_path),
+                                                          'closed.').format(path=new_path),
                                                 defaultButton=QtWidgets.QMessageBox.No)
         if answer != QtWidgets.QMessageBox.Yes:
             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(new_path)
         # Save the new location.
-        self.main_window.set_new_data_path(new_data_path)
+        self.main_window.set_new_data_path(new_path)
         self.data_directory_cancel_button.show()
 
     def on_data_directory_copy_check_box_toggled(self):
@@ -519,9 +525,11 @@
     def check_data_overwrite(self, data_path):
         """
         Check if there's already data in the target directory.
+        
+        :param data_path: The target directory to check
+        :type data_path: Path
         """
-        test_path = os.path.join(data_path, 'songs')
-        if os.path.exists(test_path):
+        if (data_path / 'songs').exists():
             self.data_exists = True
             # Check is they want to replace existing data.
             answer = QtWidgets.QMessageBox.warning(self,
@@ -530,7 +538,7 @@
                                                              'WARNING: \n\nThe location you have selected \n\n{path}'
                                                              '\n\nappears to contain OpenLP data files. Do you wish to '
                                                              'replace these files with the current data '
-                                                             'files?').format(path=os.path.abspath(data_path,)),
+                                                             'files?'.format(path=data_path.resolve())),
                                                    QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes |
                                                                                          QtWidgets.QMessageBox.No),
                                                    QtWidgets.QMessageBox.No)

=== modified file 'openlp/core/ui/exceptionform.py'
--- openlp/core/ui/exceptionform.py	2016-12-31 11:01:36 +0000
+++ openlp/core/ui/exceptionform.py	2017-06-19 07:33:18 +0000
@@ -29,6 +29,8 @@
 
 import bs4
 import sqlalchemy
+from patches.utils import str_to_path
+from patches.pyqt5patches import PQFileDialog
 from PyQt5 import Qt, QtCore, QtGui, QtWebKit, QtWidgets
 from lxml import etree
 
@@ -139,31 +141,21 @@
         """
         Saving exception log and system information to a file.
         """
-        filename = QtWidgets.QFileDialog.getSaveFileName(
+        file_path, filter_used = PQFileDialog.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))
+            Settings().path_value(self.settings_section + '/last directory'),
+            translate('OpenLP.ExceptionForm', 'Text files (*.txt *.log *.text)'))
+        if file_path:
+            Settings().set_path_value(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'])
             try:
-                report_file = open(filename, 'w')
-                try:
+                with file_path.open('w') as report_file:
                     report_file.write(report_text)
-                except UnicodeError:
-                    report_file.close()
-                    report_file = open(filename, 'wb')
-                    report_file.write(report_text.encode('utf-8'))
-                finally:
-                    report_file.close()
             except IOError:
                 log.exception('Failed to write crash report')
-            finally:
-                report_file.close()
 
     def on_send_report_button_clicked(self):
         """
@@ -212,17 +204,16 @@
 
     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 = \
+            PQFileDialog.getOpenFileName(self,
+                                         translate('ImagePlugin.ExceptionDialog', 'Select Attachment'),
+                                         Settings().path_value(self.settings_section + '/last directory'),
+                                         '{text} (*)'.format(text=UiStrings().AllFiles))
+        log.info('New files {file_path}'.format(file_path=file_path))
+        if file_path:
+            self.file_attachment = str(file_path)
 
     def __button_state(self, state):
         """

=== modified file 'openlp/core/ui/firsttimeform.py'
--- openlp/core/ui/firsttimeform.py	2017-05-30 18:42:35 +0000
+++ openlp/core/ui/firsttimeform.py	2017-06-19 07:33:18 +0000
@@ -29,8 +29,9 @@
 import urllib.request
 import urllib.parse
 import urllib.error
+from configparser import ConfigParser, MissingSectionHeaderError, NoSectionError, NoOptionError
+from pathlib2 import Path
 from tempfile import gettempdir
-from configparser import ConfigParser, MissingSectionHeaderError, NoSectionError, NoOptionError
 
 from PyQt5 import QtCore, QtWidgets
 
@@ -283,7 +284,7 @@
         self.no_internet_cancel_button.setVisible(False)
         # Check if this is a re-run of the wizard.
         self.has_run_wizard = Settings().value('core/has run wizard')
-        check_directory_exists(os.path.join(gettempdir(), 'openlp'))
+        check_directory_exists(Path(gettempdir(), 'openlp'))
 
     def update_screen_list_combo(self):
         """
@@ -403,7 +404,7 @@
             screenshot = self.config.get('theme_{theme}'.format(theme=theme), 'screenshot')
             item = self.themes_list_widget.item(index)
             if item:
-                item.setIcon(build_icon(os.path.join(gettempdir(), 'openlp', screenshot)))
+                item.setIcon(build_icon(Path(gettempdir(), 'openlp', screenshot)))
 
     def _download_progress(self, count, block_size):
         """
@@ -553,7 +554,8 @@
         Download selected songs, bibles and themes. Returns False on download error
         """
         # Build directories for downloads
-        songs_destination = os.path.join(gettempdir(), 'openlp')
+        # TODO: convert gettempdir to PathObject
+        songs_destination = Path(gettempdir(), 'openlp')
         bibles_destination = AppLocation.get_section_data_path('bibles')
         themes_destination = AppLocation.get_section_data_path('themes')
         missed_files = []
@@ -564,7 +566,7 @@
                 filename, sha256 = item.data(QtCore.Qt.UserRole)
                 self._increment_progress_bar(self.downloading.format(name=filename), 0)
                 self.previous_size = 0
-                destination = os.path.join(songs_destination, str(filename))
+                destination = songs_destination / str(filename)
                 if not url_get_file(self, '{path}{name}'.format(path=self.songs_url, name=filename),
                                     destination, sha256):
                     missed_files.append('Song: {name}'.format(name=filename))
@@ -576,9 +578,8 @@
                 bible, sha256 = item.data(0, QtCore.Qt.UserRole)
                 self._increment_progress_bar(self.downloading.format(name=bible), 0)
                 self.previous_size = 0
-                if not url_get_file(self, '{path}{name}'.format(path=self.bibles_url, name=bible),
-                                    os.path.join(bibles_destination, bible),
-                                    sha256):
+                destination = bibles_destination / bible
+                if not url_get_file(self, '{path}{name}'.format(path=self.bibles_url, name=bible), destination, sha256):
                     missed_files.append('Bible: {name}'.format(name=bible))
             bibles_iterator += 1
         # Download themes
@@ -588,9 +589,8 @@
                 theme, sha256 = item.data(QtCore.Qt.UserRole)
                 self._increment_progress_bar(self.downloading.format(name=theme), 0)
                 self.previous_size = 0
-                if not url_get_file(self, '{path}{name}'.format(path=self.themes_url, name=theme),
-                                    os.path.join(themes_destination, theme),
-                                    sha256):
+                destination = themes_destination / theme
+                if not url_get_file(self, '{path}{name}'.format(path=self.themes_url, name=theme), destination, sha256):
                     missed_files.append('Theme: {name}'.format(name=theme))
         if missed_files:
             file_list = ''

=== 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-06-19 07:33:18 +0000
@@ -23,6 +23,7 @@
 The general tab of the configuration dialog.
 """
 import logging
+from pathlib2 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,9 +268,9 @@
         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.filters = '{text};;{names} (*)'.format(
-            text=get_images_filter(), names=UiStrings().AllFiles)
+        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)
 
     def load(self):
         """
@@ -291,7 +293,7 @@
         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 = settings.value('logo file')
+        self.logo_file_path_edit.path = settings.path_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'))
@@ -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.set_path_value('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())

=== modified file 'openlp/core/ui/lib/dockwidget.py'
--- openlp/core/ui/lib/dockwidget.py	2016-12-31 11:01:36 +0000
+++ openlp/core/ui/lib/dockwidget.py	2017-06-19 07:33:18 +0000
@@ -45,6 +45,7 @@
         super(OpenLPDockWidget, self).__init__(parent)
         if name:
             self.setObjectName(name)
+        # TODO: icon to Path object?
         if icon:
             self.setWindowIcon(build_icon(icon))
         # Sort out the minimum width.

=== modified file 'openlp/core/ui/lib/listpreviewwidget.py'
--- openlp/core/ui/lib/listpreviewwidget.py	2016-12-31 11:01:36 +0000
+++ openlp/core/ui/lib/listpreviewwidget.py	2017-06-19 07:33:18 +0000
@@ -23,6 +23,7 @@
 The :mod:`listpreviewwidget` is a widget that lists the slides in the slide controller.
 It is based on a QTableWidget but represents its contents in list form.
 """
+from pathlib2 import Path
 
 from PyQt5 import QtCore, QtGui, QtWidgets
 
@@ -160,12 +161,14 @@
                     label.setScaledContents(True)
                 if self.service_item.is_command():
                     if self.service_item.is_capable(ItemCapabilities.HasThumbnails):
-                        image = self.image_manager.get_image(frame['image'], ImageSource.CommandPlugins)
+                        # TODO: frame['image'] to Path object
+                        image = self.image_manager.get_image(Path(frame['image']), ImageSource.CommandPlugins)
                         pixmap = QtGui.QPixmap.fromImage(image)
                     else:
                         pixmap = QtGui.QPixmap(frame['image'])
                 else:
-                    image = self.image_manager.get_image(frame['path'], ImageSource.ImagePlugin)
+                    # TODO: frame['path'] to Path object
+                    image = self.image_manager.get_image(Path(frame['path']), ImageSource.ImagePlugin)
                     pixmap = QtGui.QPixmap.fromImage(image)
                 pixmap.setDevicePixelRatio(label.devicePixelRatio())
                 label.setPixmap(pixmap)

=== modified file 'openlp/core/ui/lib/listwidgetwithdnd.py'
--- openlp/core/ui/lib/listwidgetwithdnd.py	2017-04-03 20:32:13 +0000
+++ openlp/core/ui/lib/listwidgetwithdnd.py	2017-06-19 07:33:18 +0000
@@ -23,6 +23,7 @@
 Extend QListWidget to handle drag and drop functionality
 """
 import os
+from pathlib2 import Path
 
 from PyQt5 import QtCore, QtGui, QtWidgets
 
@@ -108,20 +109,19 @@
 
         :param event:  Handle of the event pint passed
         """
+        # TODO: Can be refactored with treewidgetwithdnd.dropEvent
         if event.mimeData().hasUrls():
             event.setDropAction(QtCore.Qt.CopyAction)
             event.accept()
-            files = []
+            file_paths = []
             for url in event.mimeData().urls():
-                local_file = os.path.normpath(url.toLocalFile())
-                if os.path.isfile(local_file):
-                    files.append(local_file)
-                elif os.path.isdir(local_file):
-                    listing = os.listdir(local_file)
-                    for file in listing:
-                        files.append(os.path.join(local_file, file))
+                local_path = Path(url.toLocalFile())
+                if local_path.is_file():
+                    file_paths.append(local_path)
+                elif local_path.is_dir():
+                    file_paths = local_path.iterdir()
             Registry().execute('{mime_data}_dnd'.format(mime_data=self.mime_data_text),
-                               {'files': files, 'target': self.itemAt(event.pos())})
+                               {'file_paths': file_paths, 'target': self.itemAt(event.pos())})
         else:
             event.ignore()
 

=== modified file 'openlp/core/ui/lib/pathedit.py'
--- openlp/core/ui/lib/pathedit.py	2017-06-09 06:06:49 +0000
+++ openlp/core/ui/lib/pathedit.py	2017-06-19 07:33:18 +0000
@@ -20,8 +20,10 @@
 # Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
 ###############################################################################
 from enum import Enum
-import os.path
+from pathlib2 import Path
 
+from patches.utils import path_to_str, str_to_path
+from patches.pyqt5patches import PQFileDialog
 from PyQt5 import QtCore, QtWidgets
 
 from openlp.core.common import UiStrings, translate
@@ -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: 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: 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: 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: Path
 
         :return: None
+        :rtype: None
         """
         if self._path != path:
             self.path = path

=== modified file 'openlp/core/ui/lib/treewidgetwithdnd.py'
--- openlp/core/ui/lib/treewidgetwithdnd.py	2016-12-31 11:01:36 +0000
+++ openlp/core/ui/lib/treewidgetwithdnd.py	2017-06-19 07:33:18 +0000
@@ -23,6 +23,7 @@
 Extend QTreeWidget to handle drag and drop functionality
 """
 import os
+from pathlib2 import Path
 
 from PyQt5 import QtCore, QtGui, QtWidgets
 
@@ -116,16 +117,15 @@
         if event.mimeData().hasUrls():
             event.setDropAction(QtCore.Qt.CopyAction)
             event.accept()
-            files = []
+            file_paths = []
             for url in event.mimeData().urls():
-                local_file = url.toLocalFile()
-                if os.path.isfile(local_file):
-                    files.append(local_file)
-                elif os.path.isdir(local_file):
-                    listing = os.listdir(local_file)
-                    for file_name in listing:
-                        files.append(os.path.join(local_file, file_name))
-            Registry().execute('%s_dnd' % self.mime_data_text, {'files': files, 'target': self.itemAt(event.pos())})
+                local_path = Path(url.toLocalFile())
+                if local_path.is_file():
+                    file_paths.append(local_path)
+                elif local_path.is_dir():
+                    file_paths = local_path.iterdir()
+            Registry().execute(
+                '%s_dnd' % self.mime_data_text, {'file_paths': file_paths, 'target': self.itemAt(event.pos())})
         elif self.allow_internal_dnd:
             event.setDropAction(QtCore.Qt.CopyAction)
             event.accept()

=== modified file 'openlp/core/ui/lib/wizard.py'
--- openlp/core/ui/lib/wizard.py	2017-06-09 06:06:49 +0000
+++ openlp/core/ui/lib/wizard.py	2017-06-19 07:33:18 +0000
@@ -25,6 +25,8 @@
 import logging
 import os
 
+from patches.utils import path_to_str, str_to_path
+from patches.pyqt5patches import PQFileDialog
 from PyQt5 import QtCore, QtGui, QtWidgets
 
 from openlp.core.common import Registry, RegistryProperties, Settings, UiStrings, translate, is_macosx
@@ -280,35 +282,54 @@
         """
         Opens a QFileDialog and saves the filename to the given editbox.
 
-        :param title: The title of the dialog (unicode).
-        :param editbox:  An editbox (QLineEdit).
+        
+        :param title: The title of the dialog
+        :type title: str
+
+        :param editbox: An instance of QLineEdit
+        :type editbox: QtWidgets.QLineEdit
+
         :param setting_name: The place where to save the last opened directory.
+        :type setting_name: str
+
         :param filters: The file extension filters. It should contain the file description
             as well as the file extension. For example::
 
                 'OpenLP 2 Databases (*.sqlite)'
+        :type filters: str
+
+        :return: None
+        :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 += '{all_files} (*)'.format(all_files=UiStrings().AllFiles)
+        file_path, filter_used = PQFileDialog.getOpenFileName(
+            self, title, str_to_path(Settings().value(self.plugin.settings_section + '/' + setting_name)),
             filters)
-        if filename:
-            editbox.setText(filename)
-        Settings().setValue(self.plugin.settings_section + '/' + setting_name, filename)
+        if file_path:
+            editbox.setText(str(file_path))
+            Settings().setValue(self.plugin.settings_section + '/' + setting_name, str(file_path))
 
     def get_folder(self, title, editbox, setting_name):
         """
         Opens a QFileDialog and saves the selected folder to the given editbox.
 
-        :param title: The title of the dialog (unicode).
-        :param editbox: An editbox (QLineEdit).
+        :param title: The title of the dialog
+        :type title: str
+
+        :param editbox: An instance of QLineEdit
+        :type editbox: QtWidgets.QLineEdit
+
         :param setting_name: The place where to save the last opened directory.
+        :type setting_name: str
+
+        :return: None
+        :rtype: None
         """
-        folder = QtWidgets.QFileDialog.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)
+        folder_path = PQFileDialog.getExistingDirectory(
+            self, title, str_to_path(Settings().value(self.plugin.settings_section + '/' + setting_name)),
+            PQFileDialog.ShowDirsOnly)
+        if folder_path:
+            editbox.setText(str(folder_path))
+            Settings().setValue(self.plugin.settings_section + '/' + setting_name, folder_path)

=== modified file 'openlp/core/ui/maindisplay.py'
--- openlp/core/ui/maindisplay.py	2017-06-04 12:26:50 +0000
+++ openlp/core/ui/maindisplay.py	2017-06-19 07:33:18 +0000
@@ -28,10 +28,9 @@
 * `http://html5demos.com/two-videos`_
 
 """
-
 import html
 import logging
-import os
+from pathlib2 import Path
 
 from PyQt5 import QtCore, QtWidgets, QtWebKit, QtWebKitWidgets, QtGui, QtMultimedia
 
@@ -259,8 +258,8 @@
             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')
-            splash_image = QtGui.QImage(image_file)
+            image_path = Settings().path_value('core/logo file')
+            splash_image = QtGui.QImage(str(image_path))
             self.initial_fame = QtGui.QImage(
                 self.screen['size'].width(),
                 self.screen['size'].height(),
@@ -334,19 +333,21 @@
         if is_win():
             self.shake_web_view()
 
-    def direct_image(self, path, background):
+    def direct_image(self, image_path, background):
         """
         API for replacement backgrounds so Images are added directly to cache.
 
-        :param path: Path to Image
+        :param image_path: Path to Image
+        :type image_path: Path
+
         :param background: The background color
         """
-        self.image_manager.add_image(path, ImageSource.ImagePlugin, background)
+        self.image_manager.add_image(image_path, ImageSource.ImagePlugin, background)
         if not hasattr(self, 'service_item'):
             return False
-        self.override['image'] = path
+        self.override['image'] = image_path
         self.override['theme'] = self.service_item.theme_data.background_filename
-        self.image(path)
+        self.image(image_path)
         # Update the preview frame.
         if self.is_live:
             self.live_controller.update_preview()
@@ -360,7 +361,7 @@
         :param path: The path to the image to be displayed. **Note**, the path is only passed to identify the image.
             If the image has changed it has to be re-added to the image manager.
         """
-        image = self.image_manager.get_image_bytes(path, ImageSource.ImagePlugin)
+        image = self.image_manager.get_image_bytes(Path(path), ImageSource.ImagePlugin)
         self.controller.media_controller.media_reset(self.controller)
         self.display_image(image)
 
@@ -435,12 +436,17 @@
             self.shake_web_view()
         return self.grab()
 
-    def build_html(self, service_item, image_path=''):
+    def build_html(self, service_item, image_path=None):
         """
         Store the service_item and build the new HTML from it. Add the HTML to the display
 
         :param service_item: The Service item to be used
+
         :param image_path: Where the image resides.
+        :type image_path: Path, None
+
+        :return: None
+        :rtype: None
         """
         self.web_loaded = False
         self.initial_fame = None
@@ -465,7 +471,7 @@
         if self.service_item.theme_data.background_type == 'image':
             if self.service_item.theme_data.background_filename:
                 self.service_item.bg_image_bytes = self.image_manager.get_image_bytes(
-                    self.service_item.theme_data.background_filename, ImageSource.Theme)
+                    Path(self.service_item.theme_data.background_filename, ImageSource.Theme))
             if image_path:
                 image_bytes = self.image_manager.get_image_bytes(image_path, ImageSource.ImagePlugin)
         created_html = build_html(self.service_item, self.screen, self.is_live, background, image_bytes,
@@ -484,10 +490,8 @@
                 service_item = ServiceItem()
                 service_item.title = 'webkit'
                 service_item.processor = 'webkit'
-                path = os.path.join(AppLocation.get_section_data_path('themes'),
-                                    self.service_item.theme_data.theme_name)
-                service_item.add_from_command(path,
-                                              self.service_item.theme_data.background_filename,
+                path = AppLocation.get_section_data_path('themes') / self.service_item.theme_data.theme_name
+                service_item.add_from_command(str(path), str(self.service_item.theme_data.background_filename),
                                               ':/media/slidecontroller_multimedia.png')
                 self.media_controller.video(DisplayControllerType.Live, service_item, video_behind_text=True)
         self._hide_mouse()

=== modified file 'openlp/core/ui/mainwindow.py'
--- openlp/core/ui/mainwindow.py	2017-05-30 18:42:35 +0000
+++ openlp/core/ui/mainwindow.py	2017-06-19 07:33:18 +0000
@@ -30,8 +30,11 @@
 from datetime import datetime
 from distutils import dir_util
 from distutils.errors import DistutilsFileError
+from patches.shutilpatches import copyfile, rmtree
+from pathlib2 import Path
 from tempfile import gettempdir
 
+from patches.pyqt5patches import PQFileDialog
 from PyQt5 import QtCore, QtGui, QtWidgets
 
 from openlp.core.common import Registry, RegistryProperties, AppLocation, LanguageManager, Settings, \
@@ -112,11 +115,11 @@
         self.recent_files_menu.setObjectName('recentFilesMenu')
         self.file_import_menu = QtWidgets.QMenu(self.file_menu)
         if not is_macosx():
-            self.file_import_menu.setIcon(build_icon(u':/general/general_import.png'))
+            self.file_import_menu.setIcon(build_icon(':/general/general_import.png'))
         self.file_import_menu.setObjectName('file_import_menu')
         self.file_export_menu = QtWidgets.QMenu(self.file_menu)
         if not is_macosx():
-            self.file_export_menu.setIcon(build_icon(u':/general/general_export.png'))
+            self.file_export_menu.setIcon(build_icon(':/general/general_export.png'))
         self.file_export_menu.setObjectName('file_export_menu')
         # View Menu
         self.view_menu = QtWidgets.QMenu(self.menu_bar)
@@ -189,7 +192,7 @@
                                             triggers=self.service_manager_contents.on_load_service_clicked)
         self.file_save_item = create_action(main_window, 'fileSaveItem', icon=':/general/general_save.png',
                                             can_shortcuts=True, category=UiStrings().File,
-                                            triggers=self.service_manager_contents.save_file)
+                                            triggers=self.service_manager_contents.decide_save_method)
         self.file_save_as_item = create_action(main_window, 'fileSaveAsItem', can_shortcuts=True,
                                                category=UiStrings().File,
                                                triggers=self.service_manager_contents.save_file_as)
@@ -305,10 +308,9 @@
         # Give QT Extra Hint that this is an About Menu Item
         self.about_item.setMenuRole(QtWidgets.QAction.AboutRole)
         if is_win():
-            self.local_help_file = os.path.join(AppLocation.get_directory(AppLocation.AppDir), 'OpenLP.chm')
+            self.local_help_file = AppLocation.get_directory(AppLocation.AppDir) / 'OpenLP.chm'
         elif is_macosx():
-            self.local_help_file = os.path.join(AppLocation.get_directory(AppLocation.AppDir),
-                                                '..', 'Resources', 'OpenLP.help')
+            self.local_help_file = AppLocation.get_directory(AppLocation.AppDir) / '..' / 'Resources' / 'OpenLP.help'
         self.user_manual_item = create_action(main_window, 'userManualItem', icon=':/system/system_help_contents.png',
                                               can_shortcuts=True, category=UiStrings().Help,
                                               triggers=self.on_help_clicked)
@@ -656,8 +658,8 @@
                 self.application.process_events()
                 plugin.first_time()
         self.application.process_events()
-        temp_dir = os.path.join(str(gettempdir()), 'openlp')
-        shutil.rmtree(temp_dir, True)
+        temp_path = Path(gettempdir(), 'openlp')
+        rmtree(temp_path, True)
 
     def on_first_time_wizard_clicked(self):
         """
@@ -766,7 +768,7 @@
         Use the Online manual in other cases. (Linux)
         """
         if is_macosx() or is_win():
-            QtGui.QDesktopServices.openUrl(QtCore.QUrl.fromLocalFile(self.local_help_file))
+            QtGui.QDesktopServices.openUrl(QtCore.QUrl.fromLocalFile(str(self.local_help_file)))
         else:
             import webbrowser
             webbrowser.open_new('http://manual.openlp.org/')
@@ -789,7 +791,7 @@
         Open data folder
         """
         path = AppLocation.get_data_path()
-        QtGui.QDesktopServices.openUrl(QtCore.QUrl.fromLocalFile(path))
+        QtGui.QDesktopServices.openUrl(QtCore.QUrl.fromLocalFile(str(path)))
 
     def on_update_theme_images(self):
         """
@@ -840,12 +842,11 @@
                                                 QtWidgets.QMessageBox.No)
         if answer == QtWidgets.QMessageBox.No:
             return
-        import_file_name, filter_used = QtWidgets.QFileDialog.getOpenFileName(
-            self,
-            translate('OpenLP.MainWindow', 'Import settings'),
-            '',
-            translate('OpenLP.MainWindow', 'OpenLP Settings (*.conf)'))
-        if not import_file_name:
+        import_file_path, filter_used = PQFileDialog.getOpenFileName(
+            parent=self,
+            caption=translate('OpenLP.MainWindow', 'Import settings'),
+            filter=translate('OpenLP.MainWindow', 'OpenLP Settings (*.conf)'))
+        if not import_file_path:
             return
         setting_sections = []
         # Add main sections.
@@ -863,12 +864,12 @@
         # Add plugin sections.
         setting_sections.extend([plugin.name for plugin in self.plugin_manager.plugins])
         # Copy the settings file to the tmp dir, because we do not want to change the original one.
-        temp_directory = os.path.join(str(gettempdir()), 'openlp')
+        temp_directory = Path(gettempdir(), 'openlp')
         check_directory_exists(temp_directory)
-        temp_config = os.path.join(temp_directory, os.path.basename(import_file_name))
-        shutil.copyfile(import_file_name, temp_config)
+        temp_config_path = temp_directory / import_file_path.name
+        copyfile(import_file_path, temp_config_path)
         settings = Settings()
-        import_settings = Settings(temp_config, Settings.IniFormat)
+        import_settings = Settings(str(temp_config_path), Settings.IniFormat)
         # Convert image files
         log.info('hook upgrade_plugin_settings')
         self.plugin_manager.hook_upgrade_plugin_settings(import_settings)
@@ -911,7 +912,8 @@
                 settings.setValue('{key}'.format(key=section_key), value)
         now = datetime.now()
         settings.beginGroup(self.header_section)
-        settings.setValue('file_imported', import_file_name)
+        # TODO: Is this setting required? I cannot find where its read!
+        settings.set_path_value('file_imported', import_file_path)
         settings.setValue('file_date_imported', now.strftime("%Y-%m-%d %H:%M"))
         settings.endGroup()
         settings.sync()
@@ -929,17 +931,15 @@
         """
         Export settings to a .conf file in INI format
         """
-        export_file_name, filter_used = QtWidgets.QFileDialog.getSaveFileName(
-            self,
-            translate('OpenLP.MainWindow', 'Export Settings File'),
-            '',
-            translate('OpenLP.MainWindow', 'OpenLP Settings (*.conf)'))
-        if not export_file_name:
+        export_file_path, filter_used = PQFileDialog.getSaveFileName(
+            parent=self,
+            caption=translate('OpenLP.MainWindow', 'Export Settings File'),
+            filter=translate('OpenLP.MainWindow', 'OpenLP Settings (*.conf)'))
+        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.with_suffix('.conf')
+        temp_path = Path(gettempdir(), 'openlp', 'exportConf.tmp')
         self.save_settings()
         setting_sections = []
         # Add main sections.
@@ -954,15 +954,15 @@
         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)
+        if temp_path.exists():
+            temp_path.unlink()
+        if export_file_path.exists():
+            export_file_path.unlink()
         settings = Settings()
         settings.remove(self.header_section)
         # Get the settings.
         keys = settings.allKeys()
-        export_settings = Settings(temp_file, Settings.IniFormat)
+        export_settings = Settings(str(temp_path), Settings.IniFormat)
         # Add a header section.
         # This is to insure it's our conf file for import.
         now = datetime.now()
@@ -995,23 +995,20 @@
         # 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)
+            with export_file_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)
+            temp_path.unlink()
         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))
+            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))
 
     def on_mode_default_item_clicked(self):
         """
@@ -1270,7 +1267,7 @@
         settings.remove('custom slide')
         settings.remove('service')
         settings.beginGroup(self.general_settings_section)
-        self.recent_files = settings.value('recent files')
+        self.recent_files = [Path(file) for file in settings.value('recent files')]
         settings.endGroup()
         settings.beginGroup(self.ui_settings_section)
         self.move(settings.value('main window position'))
@@ -1294,7 +1291,8 @@
         log.debug('Saving QSettings')
         settings = Settings()
         settings.beginGroup(self.general_settings_section)
-        settings.setValue('recent files', self.recent_files)
+        # TODO: Look at how this is stored!
+        settings.setValue('recent files', [str(file_path) for file_path in self.recent_files])
         settings.endGroup()
         settings.beginGroup(self.ui_settings_section)
         settings.setValue('main window position', self.pos())
@@ -1310,51 +1308,42 @@
         Updates the recent file menu with the latest list of service files accessed.
         """
         recent_file_count = Settings().value('advanced/recent file count')
-        existing_recent_files = [recentFile for recentFile in self.recent_files if os.path.isfile(str(recentFile))]
+        existing_recent_files = [recent_file for recent_file in self.recent_files if recent_file.is_file()]
         recent_files_to_display = existing_recent_files[0:recent_file_count]
         self.recent_files_menu.clear()
-        for file_id, filename in enumerate(recent_files_to_display):
-            log.debug('Recent file name: {name}'.format(name=filename))
-            action = create_action(self, '',
-                                   text='&{n} {name}'.format(n=file_id + 1,
-                                                             name=os.path.splitext(os.path.basename(str(filename)))[0]),
-                                   data=filename,
-                                   triggers=self.service_manager_contents.on_recent_service_clicked)
+        for file_id, file_path in enumerate(recent_files_to_display):
+            log.debug('Recent file name: {name}'.format(name=file_path))
+            action = create_action(self, '', text='&{n} {name}'.format(n=file_id + 1, name=file_path.stem),
+                                   data=file_path, triggers=self.service_manager_contents.on_recent_service_clicked)
             self.recent_files_menu.addAction(action)
         clear_recent_files_action = create_action(self, '',
-                                                  text=translate('OpenLP.MainWindow', 'Clear List', 'Clear List of '
-                                                                                                    'recent files'),
+                                                  text=translate('OpenLP.MainWindow', 'Clear List',
+                                                                 'Clear List of recent files'),
                                                   statustip=translate('OpenLP.MainWindow', 'Clear the list of recent '
                                                                                            'files.'),
-                                                  enabled=bool(self.recent_files),
                                                   triggers=self.clear_recent_file_menu)
         add_actions(self.recent_files_menu, (None, clear_recent_files_action))
         clear_recent_files_action.setEnabled(bool(self.recent_files))
 
-    def add_recent_file(self, filename):
+    def add_recent_file(self, file_path):
         """
         Adds a service to the list of recently used files.
 
-        :param filename: The service filename to add
+        :param file_path: The service file to add
+        :type file_path: Path
+        
+        :return: None
+        :rtype: None
         """
         # The max_recent_files value does not have an interface and so never gets
         # actually stored in the settings therefore the default value of 20 will
         # always be used.
         max_recent_files = Settings().value('advanced/max recent files')
-        if filename:
-            # Add some cleanup to reduce duplication in the recent file list
-            filename = os.path.abspath(filename)
-            # abspath() only capitalises the drive letter if it wasn't provided
-            # in the given filename which then causes duplication.
-            if filename[1:3] == ':\\':
-                filename = filename[0].upper() + filename[1:]
-            if filename in self.recent_files:
-                self.recent_files.remove(filename)
-            if not isinstance(self.recent_files, list):
-                self.recent_files = [self.recent_files]
-            self.recent_files.insert(0, filename)
-            while len(self.recent_files) > max_recent_files:
-                self.recent_files.pop()
+        if file_path in self.recent_files:
+            self.recent_files.remove(file_path)
+        self.recent_files.insert(0, file_path)
+        if len(self.recent_files) > max_recent_files:
+            del self.recent_files[max_recent_files - 1:-1]
 
     def clear_recent_file_menu(self):
         """
@@ -1398,6 +1387,12 @@
     def set_new_data_path(self, new_data_path):
         """
         Set the new data path
+
+        :param new_data_path: The new path to use
+        :type new_data_path: Path
+
+        :return: None
+        :rtype: None
         """
         self.new_data_path = new_data_path
 
@@ -1412,7 +1407,7 @@
         Change the data directory.
         """
         log.info('Changing data path to {newpath}'.format(newpath=self.new_data_path))
-        old_data_path = str(AppLocation.get_data_path())
+        old_data_path = AppLocation.get_data_path()
         # Copy OpenLP data to new location if requested.
         self.application.set_busy_cursor()
         if self.copy_data:
@@ -1421,7 +1416,7 @@
                 self.show_status_message(
                     translate('OpenLP.MainWindow', 'Copying OpenLP data to new data directory location - {path} '
                               '- Please wait for copy to finish').format(path=self.new_data_path))
-                dir_util.copy_tree(old_data_path, self.new_data_path)
+                dir_util.copy_tree(str(old_data_path), str(self.new_data_path))
                 log.info('Copy successful')
             except (IOError, os.error, DistutilsFileError) as why:
                 self.application.set_normal_cursor()
@@ -1436,7 +1431,7 @@
             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.set_path_value('advanced/data path', self.new_data_path)
         # Check if the new data path is our default.
         if self.new_data_path == AppLocation.get_directory(AppLocation.DataDir):
             settings.remove('advanced/data path')

=== modified file 'openlp/core/ui/media/mediacontroller.py'
--- openlp/core/ui/media/mediacontroller.py	2017-05-30 18:50:39 +0000
+++ openlp/core/ui/media/mediacontroller.py	2017-06-19 07:33:18 +0000
@@ -388,13 +388,13 @@
         controller.media_info.is_background = video_behind_text
         # background will always loop video.
         controller.media_info.can_loop_playback = video_behind_text
-        controller.media_info.file_info = QtCore.QFileInfo(service_item.get_frame_path())
+        controller.media_info.file_info = QtCore.QFileInfo(str(service_item.get_frame_path()))
         display = self._define_display(controller)
         if controller.is_live:
             # if this is an optical device use special handling
             if service_item.is_capable(ItemCapabilities.IsOptical):
                 log.debug('video is optical and live')
-                path = service_item.get_frame_path()
+                path = str(service_item.get_frame_path())
                 (name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(path)
                 is_valid = self.media_setup_optical(name, title, audio_track, subtitle_track, start, end, display,
                                                     controller)
@@ -414,7 +414,7 @@
         elif controller.preview_display:
             if service_item.is_capable(ItemCapabilities.IsOptical):
                 log.debug('video is optical and preview')
-                path = service_item.get_frame_path()
+                path = str(service_item.get_frame_path())
                 (name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(path)
                 is_valid = self.media_setup_optical(name, title, audio_track, subtitle_track, start, end, display,
                                                     controller)
@@ -459,7 +459,7 @@
         """
         media_info = MediaInfo()
         media_info.volume = 0
-        media_info.file_info = QtCore.QFileInfo(service_item.get_frame_path())
+        media_info.file_info = QtCore.QFileInfo(str(service_item.get_frame_path()))
         # display = controller.preview_display
         suffix = '*.%s' % media_info.file_info.suffix().lower()
         used_players = get_media_players()[0]
@@ -471,7 +471,7 @@
                 translate('MediaPlugin.MediaItem', 'File {file_path} not supported using player {player_name}'
                           ).format(file_path=service_item.get_frame_path(), player_name=used_players[0]))
             return False
-        media_data = MediaInfoWrapper.parse(service_item.get_frame_path())
+        media_data = MediaInfoWrapper.parse(str(service_item.get_frame_path()))
         # duration returns in milli seconds
         service_item.set_media_length(media_data.tracks[0].duration)
         return True

=== modified file 'openlp/core/ui/printserviceform.py'
--- openlp/core/ui/printserviceform.py	2017-06-04 12:14:23 +0000
+++ openlp/core/ui/printserviceform.py	2017-06-19 07:33:18 +0000
@@ -176,7 +176,7 @@
         html_data = self._add_element('html')
         self._add_element('head', parent=html_data)
         self._add_element('title', self.title_line_edit.text(), html_data.head)
-        css_path = os.path.join(AppLocation.get_data_path(), 'serviceprint', 'service_print.css')
+        css_path = AppLocation.get_data_path() / 'serviceprint' / 'service_print.css'
         custom_css = get_text_file_string(css_path)
         if not custom_css:
             custom_css = DEFAULT_CSS

=== modified file 'openlp/core/ui/serviceitemeditform.py'
--- openlp/core/ui/serviceitemeditform.py	2017-06-04 12:14:23 +0000
+++ openlp/core/ui/serviceitemeditform.py	2017-06-19 07:33:18 +0000
@@ -22,6 +22,8 @@
 """
 The service item edit dialog
 """
+from pathlib2 import Path
+
 from PyQt5 import QtCore, QtWidgets
 
 from openlp.core.common import Registry, RegistryProperties
@@ -63,7 +65,8 @@
             self.item._raw_frames = []
             if self.item.is_image():
                 for item in self.item_list:
-                    self.item.add_from_image(item['path'], item['title'])
+                    # TODO: To image path
+                    self.item.add_from_image(Path(item['path']), item['title'])
             self.item.render()
         return self.item
 

=== modified file 'openlp/core/ui/servicemanager.py'
--- openlp/core/ui/servicemanager.py	2017-05-21 07:11:36 +0000
+++ openlp/core/ui/servicemanager.py	2017-06-19 07:33:18 +0000
@@ -28,6 +28,10 @@
 import shutil
 import zipfile
 from datetime import datetime, timedelta
+from patches.pyqt5patches import PQFileDialog
+from patches.shutilpatches import copy, rmtree
+from patches.utils import path_to_str, str_to_path
+from pathlib2 import Path
 from tempfile import mkstemp
 
 from PyQt5 import QtCore, QtGui, QtWidgets
@@ -317,8 +321,9 @@
         self.drop_position = -1
         self.service_id = 0
         # is a new service and has not been saved
-        self._modified = False
-        self._file_name = ''
+        self._file_name = None
+        self._save_lite = False
+        self.set_modified(False)
         self.service_has_all_original_files = True
         self.list_double_clicked = False
 
@@ -349,7 +354,8 @@
         if modified:
             self.service_id += 1
         self._modified = modified
-        service_file = self.short_file_name() or translate('OpenLP.ServiceManager', 'Untitled Service')
+        service_file = self.file_name().name if self.file_name() else \
+            translate('OpenLP.ServiceManager', 'Untitled Service')
         self.main_window.set_service_modified(modified, service_file)
 
     def is_modified(self):
@@ -358,29 +364,30 @@
         """
         return self._modified
 
-    def set_file_name(self, file_name):
+    def set_file_name(self, file_path):
         """
         Setter for service file.
 
-        :param file_name: The service file name
+        :param file_path: The service file path
+        :type file_path: Path
+        
+        :return: None
+        :rtype: None
         """
-        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)
-        self._save_lite = self._file_name.endswith('.oszl')
+        self._file_name = file_path
+        self.main_window.set_service_modified(self.is_modified(), file_path.name)
+        Settings().set_path_value('servicemanager/last file', file_path)
+        self._save_lite = self._file_name.suffix == '.oszl'
 
     def file_name(self):
         """
         Return the current file name including path.
+        
+        :return: The service path and file name
+        :rtype: Path
         """
         return self._file_name
 
-    def short_file_name(self):
-        """
-        Return the current file name, excluding the path.
-        """
-        return split_filename(self._file_name)[1]
-
     def reset_supported_suffixes(self):
         """
         Resets the Suffixes list.
@@ -415,11 +422,15 @@
                     return False
         self.new_file()
 
-    def on_load_service_clicked(self, load_file=None):
+    def on_load_service_clicked(self, load_path=None):
         """
         Loads the service file and saves the existing one it there is one unchanged.
 
-        :param load_file: The service file to the loaded.  Will be None is from menu so selection will be required.
+        :param load_path: The service file to the loaded.  Will be None is from menu so selection will be required.
+        :type load_path: Path
+
+        :return: False if the service is not loaded else None
+        :rtype: None or False
         """
         if self.is_modified():
             result = self.save_modified_service()
@@ -427,19 +438,17 @@
                 return False
             elif result == QtWidgets.QMessageBox.Save:
                 self.decide_save_method()
-        if not load_file:
-            file_name, filter_used = QtWidgets.QFileDialog.getOpenFileName(
+        if not load_path:
+            load_path, filter_used = PQFileDialog.getOpenFileName(
                 self.main_window,
                 translate('OpenLP.ServiceManager', 'Open File'),
-                Settings().value(self.main_window.service_manager_settings_section + '/last directory'),
+                Settings().path_value(self.main_window.service_manager_settings_section + '/last directory'),
                 translate('OpenLP.ServiceManager', 'OpenLP Service Files (*.osz *.oszl)'))
-            if not file_name:
+            if not load_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)
+        Settings().set_path_value(self.main_window.service_manager_settings_section + '/last directory',
+                            load_path.parent)
+        self.load_file(load_path)
 
     def save_modified_service(self):
         """
@@ -453,10 +462,15 @@
                                               QtWidgets.QMessageBox.Save | QtWidgets.QMessageBox.Discard |
                                               QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Save)
 
-    def on_recent_service_clicked(self, field=None):
+    def on_recent_service_clicked(self, checked=None):
         """
         Load a recent file as the service triggered by mainwindow recent service list.
-        :param field:
+
+        :param checked: Sent by the QAction.triggered signal. It's not used in this method.
+        :type checked: bool
+
+        :return: None
+        :rtype: None
         """
         sender = self.sender()
         self.load_file(sender.data())
@@ -467,10 +481,9 @@
         """
         self.service_manager_list.clear()
         self.service_items = []
-        self.set_file_name('')
         self.service_id += 1
         self.set_modified(False)
-        Settings().setValue('servicemanager/last file', '')
+        Settings().set_path_value('servicemanager/last file', None)
         self.plugin_manager.new_service_created()
 
     def create_basic_service(self):
@@ -487,26 +500,28 @@
         service.append({'openlp_core': core})
         return service
 
-    def save_file(self, field=None):
+    def save_file(self):
         """
         Save the current service file.
 
         A temporary file is created so that we don't overwrite the existing one and leave a mangled service file should
         there be an error when saving. Audio files are also copied into the service manager directory, and then packaged
         into the zip file.
+        
+        :return: None
+        :rtype: None
         """
-        if not self.file_name():
-            return self.save_file_as()
         temp_file, temp_file_name = mkstemp('.osz', 'openlp_')
+        temp_file_path = Path(temp_file_name)
         # We don't need the file handle.
+        # TODO: use gile handle instead of name
         os.close(temp_file)
-        self.log_debug(temp_file_name)
-        path_file_name = str(self.file_name())
-        path, file_name = os.path.split(path_file_name)
-        base_name = os.path.splitext(file_name)[0]
+        self.log_debug(temp_file_path)
+        base_name = self.file_name().stem
         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)
+        self.log_debug('ServiceManager.save_file - {name}'.format(name=self.file_name()))
+        Settings().set_path_value(
+            self.main_window.service_manager_settings_section + '/last directory', self.file_name().parent)
         service = self.create_basic_service()
         write_list = []
         missing_list = []
@@ -520,7 +535,7 @@
             if not item['service_item'].uses_file():
                 continue
             for frame in item['service_item'].get_frames():
-                path_from = item['service_item'].get_frame_path(frame=frame)
+                path_from = str(item['service_item'].get_frame_path(frame=frame))
                 if path_from in write_list or path_from in missing_list:
                     continue
                 if not os.path.exists(path_from):
@@ -549,10 +564,12 @@
             else:
                 service_item = item['service_item'].get_service_repr(self._save_lite)
                 if service_item['header']['background_audio']:
+                    # TODO: To path object
                     for i, file_name in enumerate(service_item['header']['background_audio']):
-                        new_file = os.path.join('audio', item['service_item'].unique_identifier, file_name)
-                        audio_files.append((file_name, new_file))
-                        service_item['header']['background_audio'][i] = new_file
+                        new_file_path = Path('audio', item['service_item'].unique_identifier, file_name)
+                        audio_files.append((Path(file_name), new_file_path))
+                        # TODO: To path object
+                        service_item['header']['background_audio'][i] = str(new_file_path)
                 # Add the service item to the service.
                 service.append({'serviceitem': service_item})
         self.repaint_service_list(-1, -1)
@@ -564,40 +581,37 @@
         # Usual Zip file cannot exceed 2GiB, file with Zip64 cannot be extracted using unzip in UNIX.
         allow_zip_64 = (total_size > 2147483648 + len(service_content))
         self.log_debug('ServiceManager.save_file - allowZip64 is {text}'.format(text=allow_zip_64))
-        zip_file = None
         success = True
         self.main_window.increment_progress_bar()
         try:
-            zip_file = zipfile.ZipFile(temp_file_name, 'w', zipfile.ZIP_STORED, allow_zip_64)
-            # First we add service contents..
-            zip_file.writestr(service_file_name, service_content)
-            # Finally add all the listed media files.
-            for write_from in write_list:
-                zip_file.write(write_from, write_from)
-            for audio_from, audio_to in audio_files:
-                if audio_from.startswith('audio'):
-                    # When items are saved, they get new unique_identifier. Let's copy the file to the new location.
-                    # Unused files can be ignored, OpenLP automatically cleans up the service manager dir on exit.
-                    audio_from = os.path.join(self.service_path, audio_from)
-                save_file = os.path.join(self.service_path, audio_to)
-                save_path = os.path.split(save_file)[0]
-                check_directory_exists(save_path)
-                if not os.path.exists(save_file):
-                    shutil.copy(audio_from, save_file)
-                zip_file.write(audio_from, audio_to)
+            # TODO: ZipFile acce
+            with zipfile.ZipFile(temp_file_name, 'w', zipfile.ZIP_STORED, allow_zip_64) as zip_file:
+                # First we add service contents..
+                zip_file.writestr(service_file_name, service_content)
+                # Finally add all the listed media files.
+                for write_from in write_list:
+                    zip_file.write(write_from, write_from)
+                for audio_from_path, audio_to_path in audio_files:
+                    if Path('audio') in audio_from_path.parents:
+                        # When items are saved, they get new unique_identifier. Let's copy the file to the new location.
+                        # Unused files can be ignored, OpenLP automatically cleans up the service manager dir on exit.
+                        audio_from_path = self.service_path / audio_from_path
+                    save_path = self.service_path / audio_to_path
+                    save_path = save_path.parent
+                    check_directory_exists(save_path)
+                    if not save_path.exists():
+                        copy(audio_from_path, save_path)
+                    zip_file.write(str(audio_from_path), str(audio_to_path))
         except IOError:
-            self.log_exception('Failed to save service to disk: {name}'.format(name=temp_file_name))
+            self.log_exception('Failed to save service to disk: {name}'.format(name=temp_file_path))
             self.main_window.error_message(translate('OpenLP.ServiceManager', 'Error Saving File'),
                                            translate('OpenLP.ServiceManager', 'There was an error saving your file.'))
             success = False
-        finally:
-            if zip_file:
-                zip_file.close()
         self.main_window.finished_progress_bar()
         self.application.set_normal_cursor()
         if success:
             try:
-                shutil.copy(temp_file_name, path_file_name)
+                copy(temp_file_path, self.file_name())
             except (shutil.Error, PermissionError):
                 return self.save_file_as()
             except OSError as ose:
@@ -606,9 +620,9 @@
                                                          'service file: {error}').format(error=ose.strerror),
                                                QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Ok))
                 success = False
-            self.main_window.add_recent_file(path_file_name)
+            self.main_window.add_recent_file(self.file_name())
             self.set_modified(False)
-        delete_file(temp_file_name)
+        delete_file(temp_file_path)
         return success
 
     def save_local_file(self):
@@ -616,18 +630,16 @@
         Save the current service file but leave all the file references alone to point to the current machine.
         This format is not transportable as it will not contain any files.
         """
-        if not self.file_name():
-            return self.save_file_as()
+        # TODO: Can this be refactored with the other `full fat` save method?
         temp_file, temp_file_name = mkstemp('.oszl', 'openlp_')
+        temp_file_path = Path(temp_file_name)
         # We don't need the file handle.
         os.close(temp_file)
-        self.log_debug(temp_file_name)
-        path_file_name = str(self.file_name())
-        path, file_name = os.path.split(path_file_name)
-        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)
+        self.log_debug(temp_file_path)
+        service_file_name = '{name}.osj'.format(name=self.file_name().stem)
+        self.log_debug('ServiceManager.save_file - {name}'.format(name=self.file_name()))
+        Settings().set_path_value(
+            self.main_window.service_manager_settings_section + '/last directory', self.file_name().parent)
         service = self.create_basic_service()
         self.application.set_busy_cursor()
         # Number of items + 1 to zip it
@@ -639,36 +651,35 @@
             service.append({'serviceitem': service_item})
             self.main_window.increment_progress_bar()
         service_content = json.dumps(service)
-        zip_file = None
         success = True
         self.main_window.increment_progress_bar()
         try:
-            zip_file = zipfile.ZipFile(temp_file_name, 'w', zipfile.ZIP_STORED, True)
-            # First we add service contents.
-            zip_file.writestr(service_file_name, service_content)
+            with zipfile.ZipFile(temp_file_name, 'w', zipfile.ZIP_STORED, True) as zip_file:
+                # First we add service contents.
+                zip_file.writestr(service_file_name, service_content)
         except IOError:
             self.log_exception('Failed to save service to disk: {name}'.format(name=temp_file_name))
             self.main_window.error_message(translate('OpenLP.ServiceManager', 'Error Saving File'),
                                            translate('OpenLP.ServiceManager', 'There was an error saving your file.'))
             success = False
-        finally:
-            if zip_file:
-                zip_file.close()
         self.main_window.finished_progress_bar()
         self.application.set_normal_cursor()
         if success:
             try:
-                shutil.copy(temp_file_name, path_file_name)
+                copy(temp_file_path, self.file_name())
             except (shutil.Error, PermissionError):
                 return self.save_file_as()
-            self.main_window.add_recent_file(path_file_name)
+            self.main_window.add_recent_file(self.file_name())
             self.set_modified(False)
-        delete_file(temp_file_name)
+        delete_file(temp_file_path)
         return success
 
-    def save_file_as(self, field=None):
+    def save_file_as(self):
         """
         Get a file name and then call :func:`ServiceManager.save_file` to save the file.
+
+        :return: None
+        :rtype: None
         """
         default_service_enabled = Settings().value('advanced/default service enabled')
         if default_service_enabled:
@@ -688,33 +699,40 @@
             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')
-        path = os.path.join(directory, default_file_name)
+        directory = Settings().path_value(self.main_window.service_manager_settings_section + '/last directory')
+        if directory:
+            suggested_path = directory / default_file_name
+        else:
+            suggested_path = Path(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.
-        if self._file_name.endswith('oszl') or self.service_has_all_original_files:
-            file_name, filter_used = QtWidgets.QFileDialog.getSaveFileName(
-                self.main_window, UiStrings().SaveService, path,
-                translate('OpenLP.ServiceManager',
-                          'OpenLP Service Files (*.osz);; OpenLP Service Files - lite (*.oszl)'))
-        else:
-            file_name, filter_used = QtWidgets.QFileDialog.getSaveFileName(
-                self.main_window, UiStrings().SaveService, path,
-                translate('OpenLP.ServiceManager', 'OpenLP Service Files (*.osz);;'))
-        if not file_name:
+        packaged_service_file = translate('OpenLP.ServiceManager', 'OpenLP Service Files - packaged (*.osz)')
+        lite_service_file = translate('OpenLP.ServiceManager', 'OpenLP Service Files - lite (*.oszl)')
+        filters = packaged_service_file
+        if (self._file_name and self._file_name.suffix == '.oszl') or self.service_has_all_original_files:
+            filters = packaged_service_file + ';;' + lite_service_file
+        file_path, filter_used = PQFileDialog.getSaveFileName(self.main_window, UiStrings().SaveService,
+                                                              suggested_path, filters)
+        if not file_path:
+            # User canceled Save dialog
             return False
-        if os.path.splitext(file_name)[1] == '':
-            file_name += '.osz'
+        if filter_used == lite_service_file:
+            file_path.with_suffix('.oszl')
         else:
-            ext = os.path.splitext(file_name)[1]
-            file_name.replace(ext, '.osz')
-        self.set_file_name(file_name)
+            # Catch all, the user can only select Packaged or lite service files
+            file_path.with_suffix('.osz')
+        self.set_file_name(file_path)
         self.decide_save_method()
 
-    def decide_save_method(self, field=None):
+    def decide_save_method(self, checked=None):
         """
         Determine which type of save method to use.
-        :param field:
+
+        :param checked: Sent by the QAction.triggered signal. It's not used in this method.
+        :type checked: bool
+
+        :return: None
+        :rtype: None
         """
         if not self.file_name():
             return self.save_file_as()
@@ -723,71 +741,70 @@
         else:
             return self.save_file()
 
-    def load_file(self, file_name):
+    def load_file(self, file_path):
         """
         Load an existing service file
-        :param file_name:
+        
+        :param file_path: Path to the file to load
+        :type file_path: Path
+        
+        :return: None
+        :rtype: None
         """
-        if not file_name:
+        if not file_path:
             return False
-        file_name = str(file_name)
-        if not os.path.exists(file_name):
+        if not file_path.exists():
             return False
         zip_file = None
-        file_to = None
         self.application.set_busy_cursor()
         try:
-            zip_file = zipfile.ZipFile(file_name)
+            zip_file = zipfile.ZipFile(str(file_path))
             for zip_info in zip_file.infolist():
                 try:
-                    ucs_file = zip_info.filename
+                    os_path = Path(zip_info.filename)
                 except UnicodeDecodeError:
                     self.log_exception('file_name "{name}" is not valid UTF-8'.format(name=zip_info.file_name))
                     critical_error_message_box(message=translate('OpenLP.ServiceManager',
                                                'File is not a valid service.\n The content encoding is not UTF-8.'))
                     continue
-                os_file = ucs_file.replace('/', os.path.sep)
-                os_file = os.path.basename(os_file)
-                self.log_debug('Extract file: {name}'.format(name=os_file))
-                zip_info.filename = os_file
-                zip_file.extract(zip_info, self.service_path)
-                if os_file.endswith('osj') or os_file.endswith('osd'):
-                    p_file = os.path.join(self.service_path, os_file)
+                os_path = os_path.parent
+                self.log_debug('Extract file: {name}'.format(name=os_path))
+                zip_info.filename = str(os_path)
+                zip_file.extract(zip_info, str(self.service_path))
+                if os_path.suffix == '.osj' or os_path.suffix == '.osd':
+                    p_path = self.service_path / os_path
             if 'p_file' in locals():
-                file_to = open(p_file, 'r')
-                if p_file.endswith('osj'):
-                    items = json.load(file_to)
-                else:
+                if p_path.suffix != '.osj':
                     critical_error_message_box(message=translate('OpenLP.ServiceManager',
                                                                  'The service file you are trying to open is in an old '
                                                                  'format.\n Please save it using OpenLP 2.0.2 or '
                                                                  'greater.'))
                     return
-                file_to.close()
+                items = json.load(p_path.read_text())
                 self.new_file()
-                self.set_file_name(file_name)
+                self.set_file_name(file_path)
                 self.main_window.display_progress_bar(len(items))
                 self.process_service_items(items)
-                delete_file(p_file)
-                self.main_window.add_recent_file(file_name)
+                delete_file(p_path)
+                self.main_window.add_recent_file(file_path)
                 self.set_modified(False)
-                Settings().setValue('servicemanager/last file', file_name)
+                Settings().set_path_value('servicemanager/last file', file_path)
             else:
                 critical_error_message_box(message=translate('OpenLP.ServiceManager', 'File is not a valid service.'))
                 self.log_error('File contains no service data')
         except (IOError, NameError):
-            self.log_exception('Problem loading service file {name}'.format(name=file_name))
+            self.log_exception('Problem loading service file {name}'.format(name=file_path))
             critical_error_message_box(message=translate('OpenLP.ServiceManager',
                                        'File could not be opened because it is corrupt.'))
         except zipfile.BadZipfile:
-            if os.path.getsize(file_name) == 0:
-                self.log_exception('Service file is zero sized: {name}'.format(name=file_name))
+            if file_path.stat().st_size == 0:
+                self.log_exception('Service file is zero sized: {name}'.format(name=file_path))
                 QtWidgets.QMessageBox.information(self, translate('OpenLP.ServiceManager', 'Empty File'),
                                                   translate('OpenLP.ServiceManager',
                                                             'This service file does not contain '
                                                             'any data.'))
             else:
-                self.log_exception('Service file is cannot be extracted as zip: {name}'.format(name=file_name))
+                self.log_exception('Service file is cannot be extracted as zip: {name}'.format(name=file_path))
                 QtWidgets.QMessageBox.information(self, translate('OpenLP.ServiceManager', 'Corrupt File'),
                                                   translate('OpenLP.ServiceManager',
                                                             'This file is either corrupt or it is not an OpenLP 2 '
@@ -795,8 +812,6 @@
             self.application.set_normal_cursor()
             return
         finally:
-            if file_to:
-                file_to.close()
             if zip_file:
                 zip_file.close()
         self.main_window.finished_progress_bar()
@@ -823,7 +838,7 @@
                 if self._save_lite:
                     service_item.set_from_service(item)
                 else:
-                    service_item.set_from_service(item, self.service_path)
+                    service_item.set_from_service(item, str(self.service_path))
                 service_item.validate_item(self.suffixes)
                 if service_item.is_capable(ItemCapabilities.OnLoadUpdate):
                     new_item = Registry().get(service_item.name).service_load(service_item)
@@ -836,9 +851,9 @@
         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')
-        if file_name:
-            self.load_file(file_name)
+        file_path = Settings().path_value('servicemanager/last file')
+        if file_path:
+            self.load_file(file_path)
 
     def context_menu(self, point):
         """
@@ -1307,11 +1322,11 @@
         """
         Empties the service_path of temporary files on system exit.
         """
-        for file_name in os.listdir(self.service_path):
-            file_path = os.path.join(self.service_path, file_name)
+        for file_path in self.service_path.iterdir():
             delete_file(file_path)
-        if os.path.exists(os.path.join(self.service_path, 'audio')):
-            shutil.rmtree(os.path.join(self.service_path, 'audio'), True)
+        audio_path = self.service_path / 'audio'
+        if audio_path.exists():
+            rmtree(audio_path, True)
 
     def on_theme_combo_box_selected(self, current_index):
         """
@@ -1596,12 +1611,11 @@
             event.setDropAction(QtCore.Qt.CopyAction)
             event.accept()
             for url in link.urls():
-                file_name = url.toLocalFile()
-                if file_name.endswith('.osz'):
-                    self.on_load_service_clicked(file_name)
-                elif file_name.endswith('.oszl'):
-                    # todo correct
-                    self.on_load_service_clicked(file_name)
+                dropped_path = Path(url.toLocalFile())
+                if dropped_path.suffix == '.osz':
+                    self.on_load_service_clicked(dropped_path)
+                elif dropped_path.suffix == '.oszl':
+                    self.on_load_service_clicked(dropped_path)
         elif link.hasText():
             plugin = link.text()
             item = self.service_manager_list.itemAt(event.pos())

=== modified file 'openlp/core/ui/settingsform.py'
--- openlp/core/ui/settingsform.py	2017-06-04 12:14:23 +0000
+++ openlp/core/ui/settingsform.py	2017-06-19 07:33:18 +0000
@@ -90,6 +90,7 @@
         # add the tab to get it to display in the correct part of the screen
         self.stacked_layout.addWidget(tab_widget)
         if is_visible:
+            # TODO: icon path to Path object?
             list_item = QtWidgets.QListWidgetItem(build_icon(tab_widget.icon_path), tab_widget.tab_title_visible)
             list_item.setData(QtCore.Qt.UserRole, tab_widget.tab_title)
             self.setting_list_widget.addItem(list_item)

=== modified file 'openlp/core/ui/slidecontroller.py'
--- openlp/core/ui/slidecontroller.py	2017-03-28 05:15:05 +0000
+++ openlp/core/ui/slidecontroller.py	2017-06-19 07:33:18 +0000
@@ -26,6 +26,7 @@
 import copy
 import os
 from collections import deque
+from pathlib2 import Path
 from threading import Lock
 
 from PyQt5 import QtCore, QtGui, QtWidgets
@@ -897,8 +898,9 @@
                 self.slide_list[str(row)] = row - 1
                 # If current slide set background to image
                 if not self.service_item.is_command() and frame_number == slide_no:
+                    # TODO: To path object
                     self.service_item.bg_image_bytes = \
-                        self.image_manager.get_image_bytes(frame['path'], ImageSource.ImagePlugin)
+                        self.image_manager.get_image_bytes(Path(frame['path']), ImageSource.ImagePlugin)
         self.preview_widget.replace_service_item(self.service_item, width, slide_no)
         self.enable_tool_bar(self.service_item)
         # Pass to display for viewing.
@@ -1173,7 +1175,8 @@
                 # If not live, use the slide's thumbnail/icon instead
                 image_path = self.service_item.get_rendered_frame(self.selected_row)
                 if self.service_item.is_capable(ItemCapabilities.HasThumbnails):
-                    image = self.image_manager.get_image(image_path, ImageSource.CommandPlugins)
+                    # TODO: Path object path
+                    image = self.image_manager.get_image(Path(image_path), ImageSource.CommandPlugins)
                     self.slide_image = QtGui.QPixmap.fromImage(image)
                 else:
                     self.slide_image = QtGui.QPixmap(image_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-06-19 07:33:18 +0000
@@ -24,6 +24,7 @@
 """
 import logging
 import os
+from pathlib2 import Path
 
 from PyQt5 import QtCore, QtGui, QtWidgets
 
@@ -401,12 +402,13 @@
             if self.theme.background_type != BackgroundType.to_string(BackgroundType.Image) and \
                     self.theme.background_type != BackgroundType.to_string(BackgroundType.Video) and \
                     self.temp_background_filename == '':
-                self.temp_background_filename = self.theme.background_filename
+                self.temp_background_filename = str(self.theme.background_filename)
                 self.theme.background_filename = ''
             if (self.theme.background_type == BackgroundType.to_string(BackgroundType.Image) or
                     self.theme.background_type != BackgroundType.to_string(BackgroundType.Video)) and \
                     self.temp_background_filename != '':
-                self.theme.background_filename = self.temp_background_filename
+                # TODO: self.temp_background_filename to path Object
+                self.theme.background_filename = Path(self.temp_background_filename)
                 self.temp_background_filename = ''
             self.set_background_page_values()
 
@@ -448,18 +450,30 @@
         """
         self.theme.background_end_color = color
 
-    def on_image_path_edit_path_changed(self, filename):
-        """
-        Background Image button pushed.
-        """
-        self.theme.background_filename = filename
+    def on_image_path_edit_path_changed(self, new_path):
+        """
+        Handle the `pathEditChanged` signal from image_path_edit
+        
+        :param new_path: Path to the new image
+        :type new_path: Path
+        
+        :return: None
+        :rtype: None
+        """
+        self.theme.background_filename = new_path
         self.set_background_page_values()
 
-    def on_video_path_edit_path_changed(self, filename):
-        """
-        Background video button pushed.
-        """
-        self.theme.background_filename = filename
+    def on_video_path_edit_path_changed(self, new_path):
+        """
+        Handle the `pathEditChanged` signal from video_path_edit
+        
+        :param new_path: Path to the new video
+        :type new_path: Path
+        
+        :return: None
+        :rtype: None
+        """
+        self.theme.background_filename = new_path
         self.set_background_page_values()
 
     def on_main_color_changed(self, color):
@@ -539,10 +553,12 @@
         save_to = None
         if self.theme.background_type == BackgroundType.to_string(BackgroundType.Image) or \
            self.theme.background_type == BackgroundType.to_string(BackgroundType.Video):
-            filename = os.path.split(str(self.theme.background_filename))[1]
+            filename = self.theme.background_filename.name
+            # TODO: self.path to Path object
             save_to = os.path.join(self.path, self.theme.theme_name, filename)
-            save_from = self.theme.background_filename
+            save_from = str(self.theme.background_filename)
         if not self.edit_mode and not self.theme_manager.check_if_theme_exists(self.theme.theme_name):
             return
-        self.theme_manager.save_theme(self.theme, save_from, save_to)
+        # TODO: to Path object
+        self.theme_manager.save_theme(self.theme, Path(save_from), Path(save_to))
         return QtWidgets.QDialog.accept(self)

=== modified file 'openlp/core/ui/thememanager.py'
--- openlp/core/ui/thememanager.py	2017-06-01 06:18:47 +0000
+++ openlp/core/ui/thememanager.py	2017-06-19 07:33:18 +0000
@@ -22,10 +22,13 @@
 """
 The Theme Manager manages adding, deleteing and modifying of themes.
 """
-import json
 import os
+import shutil
 import zipfile
-import shutil
+from patches.pyqt5patches import PQFileDialog
+from patches.shutilpatches import copyfile, rmtree
+from patches.utils import path_to_str, str_to_path
+from pathlib2 import Path
 
 from xml.etree.ElementTree import ElementTree, XML
 from PyQt5 import QtCore, QtGui, QtWidgets
@@ -134,7 +137,7 @@
         self.settings_section = 'themes'
         # Variables
         self.theme_list = []
-        self.old_background_image = None
+        self.old_background_image_path = None
 
     def bootstrap_initialise(self):
         """
@@ -150,7 +153,8 @@
         process the bootstrap post setup request
         """
         self.theme_form = ThemeForm(self)
-        self.theme_form.path = self.path
+        # TODO: to path object
+        self.theme_form.path = str(self.theme_path)
         self.file_rename_form = FileRenameForm()
         Registry().register_function('theme_update_global', self.change_global_from_tab)
         self.load_themes()
@@ -158,10 +162,13 @@
     def build_theme_path(self):
         """
         Set up the theme path variables
+
+        :return: None
+        :rtype: None
         """
-        self.path = AppLocation.get_section_data_path(self.settings_section)
-        check_directory_exists(self.path)
-        self.thumb_path = os.path.join(self.path, 'thumbnails')
+        self.theme_path = AppLocation.get_section_data_path(self.settings_section)
+        check_directory_exists(self.theme_path)
+        self.thumb_path = self.theme_path / 'thumbnails'
         check_directory_exists(self.thumb_path)
 
     def check_list_state(self, item, field=None):
@@ -269,7 +276,7 @@
                     return
                 if self.check_if_theme_exists(new_theme_name):
                     old_theme_data = self.get_theme_data(old_theme_name)
-                    self.clone_theme_data(old_theme_data, new_theme_name)
+                    self.clone_theme_data(old_theme_data, Path(new_theme_name))
                     self.delete_theme(old_theme_name)
                     for plugin in self.plugin_manager.plugins:
                         if plugin.uses_theme(old_theme_name):
@@ -288,26 +295,33 @@
                                                                'Copy of {name}',
                                                                'Copy of <theme name>').format(name=old_theme_name))
         if self.file_rename_form.exec(True):
+            # TODO: To Path object
             new_theme_name = self.file_rename_form.file_name_edit.text()
             if self.check_if_theme_exists(new_theme_name):
                 theme_data = self.get_theme_data(old_theme_name)
-                self.clone_theme_data(theme_data, new_theme_name)
+                self.clone_theme_data(theme_data, Path(new_theme_name))
 
-    def clone_theme_data(self, theme_data, new_theme_name):
+    def clone_theme_data(self, theme_data, new_theme_path):
         """
         Takes a theme and makes a new copy of it as well as saving it.
 
         :param theme_data: The theme to be used
-        :param new_theme_name: The new theme name to save the data to
+        :type theme_data: Theme
+
+        :param new_theme_path: The new theme name to save the data to
+        :type new_theme_path: Path
+
+        :return: None
+        :rtype: None
         """
-        save_to = None
-        save_from = None
+        destination_path = None
+        source_path = None
         if theme_data.background_type == 'image' or theme_data.background_type == 'video':
-            save_to = os.path.join(self.path, new_theme_name, os.path.split(str(theme_data.background_filename))[1])
-            save_from = theme_data.background_filename
-        theme_data.theme_name = new_theme_name
-        theme_data.extend_image_filename(self.path)
-        self.save_theme(theme_data, save_from, save_to)
+            destination_path = self.theme_path / new_theme_path / theme_data.background_filename.name
+            source_path = theme_data.background_filename
+        theme_data.theme_name = new_theme_path
+        theme_data.extend_image_filename(self.theme_path)
+        self.save_theme(theme_data, source_path, destination_path)
         self.load_themes()
 
     def on_edit_theme(self, field=None):
@@ -321,10 +335,10 @@
             item = self.theme_list_widget.currentItem()
             theme = self.get_theme_data(item.data(QtCore.Qt.UserRole))
             if theme.background_type == 'image' or theme.background_type == 'video':
-                self.old_background_image = theme.background_filename
+                self.old_background_image_path = theme.background_filename
             self.theme_form.theme = theme
             self.theme_form.exec(True)
-            self.old_background_image = None
+            self.old_background_image_path = None
             self.renderer.update_theme(theme.theme_name)
             self.load_themes()
 
@@ -354,17 +368,11 @@
         """
         self.theme_list.remove(theme)
         thumb = '{name}.png'.format(name=theme)
-        delete_file(os.path.join(self.path, thumb))
-        delete_file(os.path.join(self.thumb_path, thumb))
+        delete_file(self.theme_path / thumb)
+        delete_file(self.thumb_path / thumb)
         try:
-            # Windows is always unicode, so no need to encode filenames
-            if is_win():
-                shutil.rmtree(os.path.join(self.path, theme))
-            else:
-                encoding = get_filesystem_encoding()
-                shutil.rmtree(os.path.join(self.path, theme).encode(encoding))
-        except OSError as os_error:
-            shutil.Error = os_error
+            rmtree(self.theme_path / theme)
+        except OSError:
             self.log_exception('Error deleting theme {name}'.format(name=theme))
 
     def on_export_theme(self, field=None):
@@ -377,16 +385,15 @@
             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 = \
+            PQFileDialog.getSaveFileName(self.main_window,
+                                         translate('OpenLP.ThemeManager', 'Save Theme - ({name})').format(name=theme),
+                                         Settings().path_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().set_path_value(self.settings_section + '/last directory export', export_path)
+            if self._export_theme(str(export_path), theme):
                 QtWidgets.QMessageBox.information(self,
                                                   translate('OpenLP.ThemeManager', 'Theme Exported'),
                                                   translate('OpenLP.ThemeManager',
@@ -399,10 +406,14 @@
         :param theme_path: Location where the zip file will be placed
         :param theme: The name of the theme to be exported
         """
+        # TODO: Accept path objects
+        theme_path = Path(theme_path)
         theme_zip = None
         try:
-            theme_zip = zipfile.ZipFile(theme_path, 'w')
-            source = os.path.join(self.path, theme)
+            # TODO: ZipFile context manager
+            theme_zip = zipfile.ZipFile(str(theme_path), 'w')
+            # TODO: source to path object
+            source = str(self.theme_path / theme)
             for files in os.walk(source):
                 for name in files[2]:
                     theme_zip.write(os.path.join(source, name), os.path.join(theme, name))
@@ -415,7 +426,7 @@
                                                                         'occurred: {err}').format(err=ose.strerror))
             if theme_zip:
                 theme_zip.close()
-                shutil.rmtree(theme_path, True)
+                rmtree(theme_path, True)
             return False
 
     def on_import_theme(self, field=None):
@@ -424,17 +435,19 @@
         those files. This process will only load version 2 themes.
         :param field:
         """
-        files = FileDialog.getOpenFileNames(self,
-                                            translate('OpenLP.ThemeManager', 'Select Theme Import File'),
-                                            Settings().value(self.settings_section + '/last directory import'),
-                                            translate('OpenLP.ThemeManager', 'OpenLP Themes (*.otz)'))
-        self.log_info('New Themes {name}'.format(name=str(files)))
-        if not files:
+        file_paths = PQFileDialog.getOpenFileNames(
+            self,
+            translate('OpenLP.ThemeManager', 'Select Theme Import File'),
+            Settings().path_value(self.settings_section + '/last directory import'),
+            translate('OpenLP.ThemeManager', 'OpenLP Themes (*.otz)'))
+        self.log_info('New Themes {name}'.format(name=str(file_paths)))
+        if not file_paths:
             return
         self.application.set_busy_cursor()
-        for file_name in files:
-            Settings().setValue(self.settings_section + '/last directory import', str(file_name))
-            self.unzip_theme(file_name, self.path)
+        for file_path in file_paths:
+            Settings().set_path_value(self.settings_section + '/last directory import', file_path)
+            # TODO: To path objects
+            self.unzip_theme(str_to_path(file_path), self.theme_path)
         self.load_themes()
         self.application.set_normal_cursor()
 
@@ -445,15 +458,16 @@
         self.application.set_busy_cursor()
         files = AppLocation.get_files(self.settings_section, '.otz')
         for theme_file in files:
-            theme_file = os.path.join(self.path, theme_file)
-            self.unzip_theme(theme_file, self.path)
+            theme_file = self.theme_path / theme_file
+            # TODO: theme_file to Path
+            self.unzip_theme(str(theme_file), self.theme_path)
             delete_file(theme_file)
         files = AppLocation.get_files(self.settings_section, '.png')
         # No themes have been found so create one
         if not files:
             theme = Theme()
             theme.theme_name = UiStrings().Default
-            self._write_theme(theme, None, None)
+            self._write_theme(theme)
             Settings().setValue(self.settings_section + '/global theme', theme.theme_name)
         self.application.set_normal_cursor()
 
@@ -469,21 +483,21 @@
         # Sort the themes by its name considering language specific
         files.sort(key=lambda file_name: get_locale_key(str(file_name)))
         # now process the file list of png files
-        for name in files:
+        for file in files:
             # check to see file is in theme root directory
-            theme = os.path.join(self.path, name)
-            if os.path.exists(theme):
-                text_name = os.path.splitext(name)[0]
+            theme_path = self.theme_path / file
+            if theme_path.exists():
+                text_name = theme_path.stem
                 if text_name == self.global_theme:
                     name = translate('OpenLP.ThemeManager', '{name} (default)').format(name=text_name)
                 else:
                     name = text_name
-                thumb = os.path.join(self.thumb_path, '{name}.png'.format(name=text_name))
+                thumb = self.thumb_path / '{name}.png'.format(name=text_name)
                 item_name = QtWidgets.QListWidgetItem(name)
-                if validate_thumb(theme, thumb):
+                if validate_thumb(theme_path, thumb):
                     icon = build_icon(thumb)
                 else:
-                    icon = create_thumb(theme, thumb)
+                    icon = create_thumb(theme_path, thumb)
                 item_name.setIcon(icon)
                 item_name.setData(QtCore.Qt.UserRole, text_name)
                 self.theme_list_widget.addItem(item_name)
@@ -507,24 +521,28 @@
         Returns a theme object from an XML or JSON file
 
         :param theme_name: Name of the theme to load from file
-        :return: The theme object.
+        :type theme_name: str
+
+        :return:  The theme object.
+        :rtype: ThemeXML
         """
+        theme_name = str(theme_name)  # TODO: why are we getting ints here?
         self.log_debug('get theme data for theme {name}'.format(name=theme_name))
-        theme_file = os.path.join(self.path, str(theme_name), str(theme_name) + '.json')
-        theme_data = get_text_file_string(theme_file)
+        theme_file_path = self.theme_path / theme_name / '{file_name}.json'.format(file_name=theme_name)
+        theme_data = get_text_file_string(theme_file_path)
         jsn = True
         if not theme_data:
-            theme_file = os.path.join(self.path, str(theme_name), str(theme_name) + '.xml')
-            theme_data = get_text_file_string(theme_file)
+            theme_file_path = theme_file_path.with_suffix('.xml')
+            theme_data = get_text_file_string(theme_file_path)
             jsn = False
         if not theme_data:
             self.log_debug('No theme data - using default theme')
             return Theme()
         else:
             if jsn:
-                return self._create_theme_from_json(theme_data, self.path)
+                return self._create_theme_from_json(theme_data, self.theme_path)
             else:
-                return self._create_theme_from_xml(theme_data, self.path)
+                return self._create_theme_from_xml(theme_data, self.theme_path)
 
     def over_write_message_box(self, theme_name):
         """
@@ -546,57 +564,56 @@
         and upgrade if necessary.
         :param file_name:
         :param directory:
+        :type directory: Path
         """
         self.log_debug('Unzipping theme {name}'.format(name=file_name))
-        theme_zip = None
-        out_file = None
         file_xml = None
         abort_import = True
         json_theme = False
         theme_name = ""
         try:
-            theme_zip = zipfile.ZipFile(file_name)
-            json_file = [name for name in theme_zip.namelist() if os.path.splitext(name)[1].lower() == '.json']
-            if len(json_file) != 1:
-                # TODO: remove XML handling at some point but would need a auto conversion to run first.
-                xml_file = [name for name in theme_zip.namelist() if os.path.splitext(name)[1].lower() == '.xml']
-                if len(xml_file) != 1:
-                    self.log_error('Theme contains "{val:d}" theme files'.format(val=len(xml_file)))
-                    raise ValidationError
-                xml_tree = ElementTree(element=XML(theme_zip.read(xml_file[0]))).getroot()
-                theme_version = xml_tree.get('version', default=None)
-                if not theme_version or float(theme_version) < 2.0:
-                    self.log_error('Theme version is less than 2.0')
-                    raise ValidationError
-                theme_name = xml_tree.find('name').text.strip()
-            else:
-                new_theme = Theme()
-                new_theme.load_theme(theme_zip.read(json_file[0]).decode("utf-8"))
-                theme_name = new_theme.theme_name
-                json_theme = True
-            theme_folder = os.path.join(directory, theme_name)
-            theme_exists = os.path.exists(theme_folder)
-            if theme_exists and not self.over_write_message_box(theme_name):
-                abort_import = True
-                return
-            else:
-                abort_import = False
-            for name in theme_zip.namelist():
-                out_name = name.replace('/', os.path.sep)
-                split_name = out_name.split(os.path.sep)
-                if split_name[-1] == '' or len(split_name) == 1:
-                    # is directory or preview file
-                    continue
-                full_name = os.path.join(directory, out_name)
-                check_directory_exists(os.path.dirname(full_name))
-                if os.path.splitext(name)[1].lower() == '.xml' or os.path.splitext(name)[1].lower() == '.json':
-                    file_xml = str(theme_zip.read(name), 'utf-8')
-                    out_file = open(full_name, 'w', encoding='utf-8')
-                    out_file.write(file_xml)
-                else:
-                    out_file = open(full_name, 'wb')
-                    out_file.write(theme_zip.read(name))
-                out_file.close()
+            with zipfile.ZipFile(file_name) as theme_zip:  # TODO: Accepts Path object in Py3.6.2
+                json_file = [name for name in theme_zip.namelist() if os.path.splitext(name)[1].lower() == '.json']
+                if len(json_file) != 1:
+                    # TODO: remove XML handling at some point but would need a auto conversion to run first.
+                    xml_file = [name for name in theme_zip.namelist() if os.path.splitext(name)[1].lower() == '.xml']
+                    if len(xml_file) != 1:
+                        self.log_error('Theme contains "{val:d}" theme files'.format(val=len(xml_file)))
+                        raise ValidationError
+                    xml_tree = ElementTree(element=XML(theme_zip.read(xml_file[0]))).getroot()
+                    theme_version = xml_tree.get('version', default=None)
+                    if not theme_version or float(theme_version) < 2.0:
+                        self.log_error('Theme version is less than 2.0')
+                        raise ValidationError
+                    theme_name = xml_tree.find('name').text.strip()
+                else:
+                    new_theme = Theme()
+                    new_theme.load_theme(theme_zip.read(json_file[0]).decode("utf-8"))
+                    theme_name = new_theme.theme_name
+                    json_theme = True
+                theme_folder = directory / theme_name
+                if theme_folder.exists() and not self.over_write_message_box(theme_name):
+                    abort_import = True
+                    return
+                else:
+                    abort_import = False
+                for zipped_file in theme_zip.namelist():
+                    zipped_file_rel_path = Path(zipped_file)
+                    split_name = zipped_file_rel_path.parts
+                    if split_name[-1] == '' or len(split_name) == 1:
+                        # is directory or preview file
+                        continue
+                    full_name = directory / zipped_file_rel_path
+                    check_directory_exists(full_name.parent)
+                    if zipped_file_rel_path.suffix.lower() == '.xml' or zipped_file_rel_path.suffix.lower() == '.json':
+                        file_xml = str(theme_zip.read(zipped_file), 'utf-8')
+                        with full_name.open('w', encoding='utf-8') as out_file:
+                            # TODO: Can be refactored when we support Py3.5
+                            out_file.write(file_xml)
+                    else:
+                        with full_name.open('wb') as out_file:
+                            # TODO: Can be refactored when we support Py3.5
+                            out_file.write(theme_zip.read(zipped_file))
         except (IOError, zipfile.BadZipfile):
             self.log_exception('Importing theme from zip failed {name}'.format(name=file_name))
             raise ValidationError
@@ -604,108 +621,110 @@
             critical_error_message_box(translate('OpenLP.ThemeManager', 'Validation Error'),
                                        translate('OpenLP.ThemeManager', 'File is not a valid theme.'))
         finally:
-            # Close the files, to be able to continue creating the theme.
-            if theme_zip:
-                theme_zip.close()
-            if out_file:
-                out_file.close()
             if not abort_import:
                 # As all files are closed, we can create the Theme.
                 if file_xml:
                     if json_theme:
-                        theme = self._create_theme_from_json(file_xml, self.path)
+                        theme = self._create_theme_from_json(file_xml, self.theme_path)
                     else:
-                        theme = self._create_theme_from_xml(file_xml, self.path)
+                        theme = self._create_theme_from_xml(file_xml, self.theme_path)
                     self.generate_and_save_image(theme_name, theme)
-                # Only show the error message, when IOError was not raised (in
-                # this case the error message has already been shown).
-                elif theme_zip is not None:
-                    critical_error_message_box(
-                        translate('OpenLP.ThemeManager', 'Validation Error'),
-                        translate('OpenLP.ThemeManager', 'File is not a valid theme.'))
-                    self.log_error('Theme file does not contain XML data {name}'.format(name=file_name))
 
     def check_if_theme_exists(self, theme_name):
         """
         Check if theme already exists and displays error message
 
         :param theme_name:  Name of the Theme to test
+
         :return: True or False if theme exists
+        :rtype: bool
         """
-        theme_dir = os.path.join(self.path, theme_name)
-        if os.path.exists(theme_dir):
+        # TODO: is theme name str?
+        theme_dir = self.theme_path / theme_name
+        if theme_dir.exists():
             critical_error_message_box(
                 translate('OpenLP.ThemeManager', 'Validation Error'),
                 translate('OpenLP.ThemeManager', 'A theme with this name already exists.'))
             return False
         return True
 
-    def save_theme(self, theme, image_from, image_to):
+    def save_theme(self, theme, image_source_path, image_destination_path):
         """
         Called by theme maintenance Dialog to save the theme and to trigger the reload of the theme list
 
+        
         :param theme: The theme data object.
-        :param image_from: Where the theme image is currently located.
-        :param image_to: Where the Theme Image is to be saved to
+        :type theme: Theme
+
+        :param image_source_path: Where the theme image is currently located.
+        :type image_source_path: pathlib2.Path
+
+        :param image_destination_path: Where the Theme Image is to be saved to
+        :type image_destination_path: pathlib2.Path
+
+        :return: None
+        :rtype: None
         """
-        self._write_theme(theme, image_from, image_to)
+        self._write_theme(theme, image_source_path, image_destination_path)
         if theme.background_type == BackgroundType.to_string(BackgroundType.Image):
             self.image_manager.update_image_border(theme.background_filename,
                                                    ImageSource.Theme,
                                                    QtGui.QColor(theme.background_border_color))
             self.image_manager.process_updates()
 
-    def _write_theme(self, theme, image_from, image_to):
+    def _write_theme(self, theme, image_source_path=None, image_destination_path=None):
         """
         Writes the theme to the disk and handles the background image if necessary
 
         :param theme: The theme data object.
-        :param image_from: Where the theme image is currently located.
-        :param image_to: Where the Theme Image is to be saved to
+        :type theme: Theme
+
+        :param image_source_path: Where the theme image is currently located.
+        :type image_source_path: pathlib2.Path
+
+        :param image_destination_path: Where the Theme Image is to be saved to
+        :type image_destination_path: pathlib2.Path
+
+        :return: None
+        :rtype: None
         """
         name = theme.theme_name
         theme_pretty = theme.export_theme()
-        theme_dir = os.path.join(self.path, name)
+        theme_dir = self.theme_path / name
         check_directory_exists(theme_dir)
-        theme_file = os.path.join(theme_dir, name + '.json')
-        if self.old_background_image and image_to != self.old_background_image:
-            delete_file(self.old_background_image)
-        out_file = None
+        theme_path = theme_dir / '{file_name}.json'.format(file_name=name)
         try:
-            out_file = open(theme_file, 'w', encoding='utf-8')
-            out_file.write(theme_pretty)
+            theme_path.write_text(theme_pretty)
         except IOError:
             self.log_exception('Saving theme to file failed')
-        finally:
-            if out_file:
-                out_file.close()
-        if image_from and os.path.abspath(image_from) != os.path.abspath(image_to):
-            try:
-                # Windows is always unicode, so no need to encode filenames
-                if is_win():
-                    shutil.copyfile(image_from, image_to)
-                else:
-                    encoding = get_filesystem_encoding()
-                    shutil.copyfile(image_from.encode(encoding), image_to.encode(encoding))
-            except IOError as xxx_todo_changeme:
-                shutil.Error = xxx_todo_changeme
-                self.log_exception('Failed to save theme image')
+        if image_source_path and image_destination_path:
+            image_source_path = image_source_path.resolve()
+            image_destination_path = image_destination_path.resolve()
+            if self.old_background_image_path and image_destination_path != self.old_background_image_path:
+                delete_file(self.old_background_image_path)
+            if image_source_path != image_destination_path:
+                try:
+                    copyfile(image_source_path, image_destination_path)
+                except IOError:
+                    self.log_exception('Failed to save theme image')
         self.generate_and_save_image(name, theme)
 
-    def generate_and_save_image(self, name, theme):
+    def generate_and_save_image(self, theme_name, theme):
         """
         Generate and save a preview image
 
-        :param name: The name of the theme.
+        :param theme_name: The name of the theme.
+        :type theme_name: str
+
         :param theme: The theme data object.
         """
         frame = self.generate_image(theme)
-        sample_path_name = os.path.join(self.path, name + '.png')
-        if os.path.exists(sample_path_name):
-            os.unlink(sample_path_name)
-        frame.save(sample_path_name, 'png')
-        thumb = os.path.join(self.thumb_path, '{name}.png'.format(name=name))
-        create_thumb(sample_path_name, thumb, False)
+        sample_path_name = self.theme_path / theme_name + '.png'
+        if sample_path_name.exists():
+            sample_path_name.unlink()
+        frame.save(str(sample_path_name), 'png')  # TODO: Where does save come from?
+        thumb_path = self.thumb_path / '{name}.png'.format(name=theme_name)
+        create_thumb(sample_path_name, thumb_path, False)
 
     def update_preview_images(self):
         """
@@ -733,7 +752,8 @@
 
         :param theme: The theme to return the image for.
         """
-        return os.path.join(self.path, theme + '.png')
+        # TODO: To path object
+        return os.path.join(self.theme_path, theme + '.png')
 
     @staticmethod
     def _create_theme_from_xml(theme_xml, image_path):
@@ -742,7 +762,10 @@
 
         :param theme_xml: The Theme data object.
         :param image_path: Where the theme image is stored
+        :type image_path: Path
+
         :return: Theme data.
+        :rtype: Theme
         """
         theme = Theme()
         theme.parse(theme_xml)
@@ -756,7 +779,10 @@
 
         :param theme_json: The Theme data object.
         :param image_path: Where the theme image is stored
+        :type image_path: Path
+
         :return: Theme data.
+        :rtype: Theme
         """
         theme = Theme()
         theme.load_theme(theme_json)

=== modified file 'openlp/core/ui/themestab.py'
--- openlp/core/ui/themestab.py	2016-12-31 11:01:36 +0000
+++ openlp/core/ui/themestab.py	2017-06-19 07:33:18 +0000
@@ -211,8 +211,8 @@
         """
         Utility method to update the global theme preview image.
         """
-        image = self.theme_manager.get_preview_image(self.global_theme)
-        preview = QtGui.QPixmap(str(image))
+        image_path = self.theme_manager.theme_path / '{file_name}.png'.format(file_name=self.global_theme)
+        preview = QtGui.QPixmap(str(image_path))
         if not preview.isNull():
             preview = preview.scaled(300, 255, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
         self.default_list_view.setPixmap(preview)

=== modified file 'openlp/plugins/alerts/alertsplugin.py'
--- openlp/plugins/alerts/alertsplugin.py	2017-05-30 18:42:35 +0000
+++ openlp/plugins/alerts/alertsplugin.py	2017-06-19 07:33:18 +0000
@@ -135,6 +135,7 @@
         """
         super(AlertsPlugin, self).__init__('alerts', __default_settings__, settings_tab_class=AlertsTab)
         self.weight = -3
+        # TODO: icon_path to Path object?
         self.icon_path = ':/plugins/plugin_alerts.png'
         self.icon = build_icon(self.icon_path)
         AlertsManager(self)

=== modified file 'openlp/plugins/alerts/forms/alertform.py'
--- openlp/plugins/alerts/forms/alertform.py	2017-06-09 06:06:49 +0000
+++ openlp/plugins/alerts/forms/alertform.py	2017-06-19 07:33:18 +0000
@@ -70,7 +70,7 @@
             item_name = QtWidgets.QListWidgetItem(alert.text)
             item_name.setData(QtCore.Qt.UserRole, alert.id)
             self.alert_list_widget.addItem(item_name)
-            if alert.text == str(self.alert_text_edit.text()):
+            if alert.text == self.alert_text_edit.text():
                 self.item_id = alert.id
                 self.alert_list_widget.setCurrentRow(self.alert_list_widget.row(item_name))
 

=== modified file 'openlp/plugins/alerts/lib/alertstab.py'
--- openlp/plugins/alerts/lib/alertstab.py	2016-12-31 11:01:36 +0000
+++ openlp/plugins/alerts/lib/alertstab.py	2017-06-19 07:33:18 +0000
@@ -32,9 +32,6 @@
     """
     AlertsTab is the alerts settings tab in the settings dialog.
     """
-    def __init__(self, parent, name, visible_title, icon_path):
-        super(AlertsTab, self).__init__(parent, name, visible_title, icon_path)
-
     def setupUi(self):
         self.setObjectName('AlertsTab')
         super(AlertsTab, self).setupUi()

=== modified file 'openlp/plugins/bibles/bibleplugin.py'
--- openlp/plugins/bibles/bibleplugin.py	2017-06-04 09:52:15 +0000
+++ openlp/plugins/bibles/bibleplugin.py	2017-06-19 07:33:18 +0000
@@ -71,6 +71,7 @@
     def __init__(self):
         super(BiblePlugin, self).__init__('bibles', __default_settings__, BibleMediaItem, BiblesTab)
         self.weight = -9
+        # TODO: icon_path to Path object?
         self.icon_path = ':/plugins/plugin_bibles.png'
         self.icon = build_icon(self.icon_path)
         self.manager = BibleManager(self)

=== modified file 'openlp/plugins/bibles/forms/bibleimportform.py'
--- openlp/plugins/bibles/forms/bibleimportform.py	2017-05-30 18:50:39 +0000
+++ openlp/plugins/bibles/forms/bibleimportform.py	2017-06-19 07:33:18 +0000
@@ -584,7 +584,6 @@
         elif self.currentPage() == self.license_details_page:
             license_version = self.field('license_version')
             license_copyright = self.field('license_copyright')
-            path = AppLocation.get_section_data_path('bibles')
             if not license_version:
                 critical_error_message_box(
                     UiStrings().EmptyField,
@@ -606,7 +605,7 @@
                               'existing one.'))
                 self.version_name_edit.setFocus()
                 return False
-            elif os.path.exists(os.path.join(path, clean_filename(license_version))):
+            elif (AppLocation.get_section_data_path('bibles') / clean_filename(license_version)).exists():
                 critical_error_message_box(
                     translate('BiblesPlugin.ImportWizardForm', 'Bible Exists'),
                     translate('BiblesPlugin.ImportWizardForm', 'This Bible already exists. Please import '

=== modified file 'openlp/plugins/bibles/lib/biblestab.py'
--- openlp/plugins/bibles/lib/biblestab.py	2016-12-31 11:01:36 +0000
+++ openlp/plugins/bibles/lib/biblestab.py	2017-06-19 07:33:18 +0000
@@ -39,11 +39,11 @@
     """
     log.info('Bible Tab loaded')
 
-    def _init_(self, parent, title, visible_title, icon_path):
+    def _init_(self, *args, **kwargs):
         self.paragraph_style = True
         self.show_new_chapters = False
         self.display_style = 0
-        super(BiblesTab, self).__init__(parent, title, visible_title, icon_path)
+        super().__init__(*args, **kwargs)
 
     def setupUi(self):
         self.setObjectName('BiblesTab')

=== modified file 'openlp/plugins/bibles/lib/db.py'
--- openlp/plugins/bibles/lib/db.py	2017-06-01 06:18:47 +0000
+++ openlp/plugins/bibles/lib/db.py	2017-06-19 07:33:18 +0000
@@ -132,7 +132,11 @@
 
             ``name``
                 The name of the database. This is also used as the file name for SQLite databases.
+
+        :return: None
+        :rtype: None
         """
+        # TODO: Accept Path objects
         log.info('BibleDB loaded')
         self._setup(parent, **kwargs)
 
@@ -470,9 +474,9 @@
         Return the cursor object. Instantiate one if it doesn't exist yet.
         """
         if BiblesResourcesDB.cursor is None:
-            file_path = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir),
-                                     'bibles', 'resources', 'bibles_resources.sqlite')
-            conn = sqlite3.connect(file_path)
+            file_path = \
+                AppLocation.get_directory(AppLocation.PluginsDir) / 'bibles' / 'resources' / 'bibles_resources.sqlite'
+            conn = sqlite3.connect(str(file_path))
             BiblesResourcesDB.cursor = conn.cursor()
         return BiblesResourcesDB.cursor
 
@@ -758,8 +762,7 @@
         If necessary loads up the database and creates the tables if the database doesn't exist.
         """
         if AlternativeBookNamesDB.cursor is None:
-            file_path = os.path.join(
-                AppLocation.get_directory(AppLocation.DataDir), 'bibles', 'alternative_book_names.sqlite')
+            file_path = str(AppLocation.get_directory(AppLocation.DataDir) / 'bibles' / 'alternative_book_names.sqlite')
             if not os.path.exists(file_path):
                 # create new DB, create table alternative_book_names
                 AlternativeBookNamesDB.conn = sqlite3.connect(file_path)

=== modified file 'openlp/plugins/bibles/lib/importers/csvbible.py'
--- openlp/plugins/bibles/lib/importers/csvbible.py	2016-12-31 11:01:36 +0000
+++ openlp/plugins/bibles/lib/importers/csvbible.py	2017-06-19 07:33:18 +0000
@@ -51,6 +51,7 @@
 """
 import csv
 from collections import namedtuple
+from pathlib2 import Path
 
 from openlp.core.common import get_file_encoding, translate
 from openlp.core.lib.exceptions import ValidationError
@@ -72,8 +73,8 @@
         """
         super().__init__(*args, **kwargs)
         self.log_info(self.__class__.__name__)
-        self.books_file = kwargs['booksfile']
-        self.verses_file = kwargs['versefile']
+        self.books_path = Path(kwargs['booksfile'])
+        self.verses_path = Path(kwargs['versefile'])
 
     @staticmethod
     def get_book_name(name, books):
@@ -91,21 +92,26 @@
         return book_name
 
     @staticmethod
-    def parse_csv_file(filename, results_tuple):
+    def parse_csv_file(file_path, results_tuple):
         """
         Parse the supplied CSV file.
 
-        :param filename: The name of the file to parse. Str
-        :param results_tuple: The namedtuple to use to store the results. namedtuple
-        :return: An iterable yielding namedtuples of type results_tuple
+        :param file_path: The name of the file to parse.
+        :type file_path: Path
+
+        :param results_tuple: The namedtuple to use to store the results.
+        :type results_tuple: namedtuple
+        
+        :return: An list of namedtuples of type results_tuple
+        :rtype: list[namedtuple]
         """
         try:
-            encoding = get_file_encoding(filename)['encoding']
-            with open(filename, 'r', encoding=encoding, newline='') as csv_file:
+            encoding = get_file_encoding(file_path)['encoding']
+            with file_path.open('r', encoding=encoding, newline='') as csv_file:
                 csv_reader = csv.reader(csv_file, delimiter=',', quotechar='"')
                 return [results_tuple(*line) for line in csv_reader]
         except (OSError, csv.Error):
-            raise ValidationError(msg='Parsing "{file}" failed'.format(file=filename))
+            raise ValidationError(msg='Parsing "{file}" failed'.format(file=file_path))
 
     def process_books(self, books):
         """
@@ -158,12 +164,12 @@
         self.language_id = self.get_language(bible_name)
         if not self.language_id:
             return False
-        books = self.parse_csv_file(self.books_file, Book)
+        books = self.parse_csv_file(self.books_path, Book)
         self.wizard.progress_bar.setValue(0)
         self.wizard.progress_bar.setMinimum(0)
         self.wizard.progress_bar.setMaximum(len(books))
         book_list = self.process_books(books)
-        verses = self.parse_csv_file(self.verses_file, Verse)
+        verses = self.parse_csv_file(self.verses_path, Verse)
         self.wizard.progress_bar.setValue(0)
         self.wizard.progress_bar.setMaximum(len(books) + 1)
         self.process_verses(verses, book_list)

=== modified file 'openlp/plugins/bibles/lib/importers/http.py'
--- openlp/plugins/bibles/lib/importers/http.py	2017-05-30 18:42:35 +0000
+++ openlp/plugins/bibles/lib/importers/http.py	2017-06-19 07:33:18 +0000
@@ -595,6 +595,7 @@
 
         Init confirms the bible exists and stores the database path.
         """
+        # TODO: Support Path objects
         super().__init__(*args, **kwargs)
         self.download_source = kwargs['download_source']
         self.download_name = kwargs['download_name']

=== modified file 'openlp/plugins/bibles/lib/manager.py'
--- openlp/plugins/bibles/lib/manager.py	2017-05-26 13:30:54 +0000
+++ openlp/plugins/bibles/lib/manager.py	2017-06-19 07:33:18 +0000
@@ -22,6 +22,7 @@
 
 import logging
 import os
+from pathlib2 import Path
 
 from openlp.core.common import AppLocation, OpenLPMixin, RegistryProperties, Settings, translate, delete_file, UiStrings
 from openlp.plugins.bibles.lib import LanguageSelection, parse_reference
@@ -111,7 +112,8 @@
         self.settings_section = 'bibles'
         self.web = 'Web'
         self.db_cache = None
-        self.path = AppLocation.get_section_data_path(self.settings_section)
+        # TODO: self.path to Path object
+        self.path = str(AppLocation.get_section_data_path(self.settings_section))
         self.proxy_name = Settings().value(self.settings_section + '/proxy name')
         self.suffix = '.sqlite'
         self.import_wizard = None
@@ -125,8 +127,8 @@
         """
         log.debug('Reload bibles')
         files = AppLocation.get_files(self.settings_section, self.suffix)
-        if 'alternative_book_names.sqlite' in files:
-            files.remove('alternative_book_names.sqlite')
+        if Path('alternative_book_names.sqlite') in files:
+            files.remove(Path('alternative_book_names.sqlite'))
         log.debug('Bible Files {text}'.format(text=files))
         self.db_cache = {}
         for filename in files:
@@ -137,7 +139,8 @@
             # Remove corrupted files.
             if name is None:
                 bible.session.close_all()
-                delete_file(os.path.join(self.path, filename))
+                # TODO: self.path to Path object
+                delete_file(Path(self.path, filename))
                 continue
             log.debug('Bible Name: "{name}"'.format(name=name))
             self.db_cache[name] = bible
@@ -185,7 +188,8 @@
         bible = self.db_cache[name]
         bible.session.close_all()
         bible.session = None
-        return delete_file(os.path.join(bible.path, bible.file))
+        # TODO: bible.path, bible.file to Path objects
+        return delete_file(Path(bible.path, bible.file))
 
     def get_bibles(self):
         """

=== modified file 'openlp/plugins/custom/customplugin.py'
--- openlp/plugins/custom/customplugin.py	2017-06-04 09:52:15 +0000
+++ openlp/plugins/custom/customplugin.py	2017-06-19 07:33:18 +0000
@@ -59,6 +59,7 @@
         super(CustomPlugin, self).__init__('custom', __default_settings__, CustomMediaItem, CustomTab)
         self.weight = -5
         self.db_manager = Manager('custom', init_schema)
+        # TODO: icon_path to Path object?
         self.icon_path = ':/plugins/plugin_custom.png'
         self.icon = build_icon(self.icon_path)
 

=== modified file 'openlp/plugins/custom/lib/customtab.py'
--- openlp/plugins/custom/lib/customtab.py	2016-12-31 11:01:36 +0000
+++ openlp/plugins/custom/lib/customtab.py	2017-06-19 07:33:18 +0000
@@ -34,9 +34,6 @@
     """
     CustomTab is the Custom settings tab in the settings dialog.
     """
-    def __init__(self, parent, title, visible_title, icon_path):
-        super(CustomTab, self).__init__(parent, title, visible_title, icon_path)
-
     def setupUi(self):
         self.setObjectName('CustomTab')
         super(CustomTab, self).setupUi()

=== modified file 'openlp/plugins/images/lib/imagetab.py'
--- openlp/plugins/images/lib/imagetab.py	2016-12-31 11:01:36 +0000
+++ openlp/plugins/images/lib/imagetab.py	2017-06-19 07:33:18 +0000
@@ -31,9 +31,6 @@
     """
     ImageTab is the images settings tab in the settings dialog.
     """
-    def __init__(self, parent, name, visible_title, icon_path):
-        super(ImageTab, self).__init__(parent, name, visible_title, icon_path)
-
     def setupUi(self):
         self.setObjectName('ImagesTab')
         super(ImageTab, self).setupUi()

=== modified file 'openlp/plugins/images/lib/mediaitem.py'
--- openlp/plugins/images/lib/mediaitem.py	2017-05-30 18:42:35 +0000
+++ openlp/plugins/images/lib/mediaitem.py	2017-06-19 07:33:18 +0000
@@ -22,7 +22,9 @@
 
 import logging
 import os
+from pathlib2 import Path
 
+from patches.utils import path_to_str
 from PyQt5 import QtCore, QtGui, QtWidgets
 
 from openlp.core.common import Registry, AppLocation, Settings, UiStrings, check_directory_exists, translate, \
@@ -98,7 +100,7 @@
         self.list_view.setIconSize(QtCore.QSize(88, 50))
         self.list_view.setIndentation(self.list_view.default_indentation)
         self.list_view.allow_internal_dnd = True
-        self.service_path = os.path.join(AppLocation.get_section_data_path(self.settings_section), 'thumbnails')
+        self.service_path = AppLocation.get_section_data_path(self.settings_section) / 'thumbnails'
         check_directory_exists(self.service_path)
         # Load images from the database
         self.load_full_list(
@@ -210,7 +212,8 @@
         """
         images = self.manager.get_all_objects(ImageFilenames, ImageFilenames.group_id == image_group.id)
         for image in images:
-            delete_file(os.path.join(self.service_path, os.path.split(image.filename)[1]))
+            # TODO: image.filename to path object
+            delete_file(self.service_path / os.path.split(image.filename)[1])
             delete_file(self.generate_thumbnail_path(image))
             self.manager.delete_object(ImageFilenames, image.id)
         image_groups = self.manager.get_all_objects(ImageGroups, ImageGroups.parent_id == image_group.id)
@@ -233,7 +236,7 @@
                 if row_item:
                     item_data = row_item.data(0, QtCore.Qt.UserRole)
                     if isinstance(item_data, ImageFilenames):
-                        delete_file(os.path.join(self.service_path, row_item.text(0)))
+                        delete_file(self.service_path / row_item.text(0))
                         delete_file(self.generate_thumbnail_path(item_data))
                         if item_data.group_id == 0:
                             self.list_view.takeTopLevelItem(self.list_view.indexOfTopLevelItem(row_item))
@@ -325,11 +328,15 @@
         """
         Generate a path to the thumbnail
 
-        :param image: An instance of ImageFileNames
-        :return: A path to the thumbnail of type str
+        :param image: The image to generate the thumbnail path for.
+        :type image: ImageFileNames
+        
+        :return: A path to the thumbnail
+        :rtype: Path
         """
-        ext = os.path.splitext(image.filename)[1].lower()
-        return os.path.join(self.service_path, '{}{}'.format(str(image.id), ext))
+        # ImageFileNames.filename to Path
+        ext = Path(image.filename).suffix.lower()
+        return self.service_path / '{name}{ext}'.format(name=str(image.id), ext=ext)
 
     def load_full_list(self, images, initial_load=False, open_group=None):
         """
@@ -352,33 +359,35 @@
         # Sort the images by its filename considering language specific.
         # characters.
         images.sort(key=lambda image_object: get_locale_key(os.path.split(str(image_object.filename))[1]))
-        for image_file in images:
-            log.debug('Loading image: {name}'.format(name=image_file.filename))
-            filename = os.path.split(image_file.filename)[1]
-            thumb = self.generate_thumbnail_path(image_file)
-            if not os.path.exists(image_file.filename):
+        for image in images:
+            log.debug('Loading image: {name}'.format(name=image.filename))
+            filename = os.path.split(image.filename)[1]
+            thumbnail_path = self.generate_thumbnail_path(image)
+            if not os.path.exists(image.filename):
                 icon = build_icon(':/general/general_delete.png')
             else:
-                if validate_thumb(image_file.filename, thumb):
-                    icon = build_icon(thumb)
+                # TODO: image_file.filename to Path object
+                if validate_thumb(Path(image.filename), thumbnail_path):
+                    icon = build_icon(thumbnail_path)
                 else:
-                    icon = create_thumb(image_file.filename, thumb)
+                    icon = create_thumb(Path(image.filename), Path(thumbnail_path))
             item_name = QtWidgets.QTreeWidgetItem([filename])
             item_name.setText(0, filename)
             item_name.setIcon(0, icon)
-            item_name.setToolTip(0, image_file.filename)
-            item_name.setData(0, QtCore.Qt.UserRole, image_file)
-            if image_file.group_id == 0:
+            item_name.setToolTip(0, image.filename)
+            item_name.setData(0, QtCore.Qt.UserRole, image)
+            if image.group_id == 0:
                 self.list_view.addTopLevelItem(item_name)
             else:
-                group_items[image_file.group_id].addChild(item_name)
+                group_items[image.group_id].addChild(item_name)
             if not initial_load:
                 self.main_window.increment_progress_bar()
         if not initial_load:
             self.main_window.finished_progress_bar()
         self.application.set_normal_cursor()
 
-    def validate_and_load(self, files, target_group=None):
+    # TODO: Duplicates code in mediamanageritem.validate_and_load
+    def validate_and_load(self, file_paths, target_group=None):
         """
         Process a list for files either from the File Dialog or from Drag and Drop.
         This method is overloaded from MediaManagerItem.
@@ -387,15 +396,17 @@
         :param target_group: The QTreeWidgetItem of the group that will be the parent of the added files
         """
         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)
+        self.load_list(file_paths, target_group)
+        last_dir = file_paths[0].parent
+        Settings().set_path_value(self.settings_section + '/last directory', last_dir)
 
-    def load_list(self, images, target_group=None, initial_load=False):
+    def load_list(self, image_paths, target_group=None, initial_load=False):
         """
         Add new images to the database. This method is called when adding images using the Add button or DnD.
 
-        :param images: A List of strings containing the filenames of the files to be loaded
+        :param image_paths: A list of file paths to the images to be loaded
+        :type image_paths: list[Path]
+
         :param target_group: The QTreeWidgetItem of the group that will be the parent of the added files
         :param initial_load: When set to False, the busy cursor and progressbar will be shown while loading images
         """
@@ -428,7 +439,7 @@
             else:
                 self.choose_group_form.existing_radio_button.setDisabled(False)
                 self.choose_group_form.group_combobox.setDisabled(False)
-            # Ask which group the images should be saved in
+            # Ask which group the image_paths should be saved in
             if self.choose_group_form.exec(selected_group=preselect_group):
                 if self.choose_group_form.nogroup_radio_button.isChecked():
                     # User chose 'No group'
@@ -460,32 +471,35 @@
             return
         # Initialize busy cursor and progress bar
         self.application.set_busy_cursor()
-        self.main_window.display_progress_bar(len(images))
-        # Save the new images in the database
-        self.save_new_images_list(images, group_id=parent_group.id, reload_list=False)
+        self.main_window.display_progress_bar(len(image_paths))
+        # Save the new image_paths in the database
+        self.save_new_images_list(image_paths, group_id=parent_group.id, reload_list=False)
         self.load_full_list(self.manager.get_all_objects(ImageFilenames, order_by_ref=ImageFilenames.filename),
                             initial_load=initial_load, open_group=parent_group)
         self.application.set_normal_cursor()
 
-    def save_new_images_list(self, images_list, group_id=0, reload_list=True):
+    def save_new_images_list(self, image_paths, group_id=0, reload_list=True):
         """
         Convert a list of image filenames to ImageFilenames objects and save them in the database.
 
-        :param images_list: A List of strings containing image filenames
+        :param image_paths: A List of file paths to image
+        :type image_paths list[Path]
+
         :param group_id: The ID of the group to save the images in
         :param reload_list: This boolean is set to True when the list in the interface should be reloaded after saving
             the new images
         """
-        for filename in images_list:
-            if not isinstance(filename, str):
+        for image_path in image_paths:
+            if not isinstance(image_path, Path):
                 continue
-            log.debug('Adding new image: {name}'.format(name=filename))
+            log.debug('Adding new image: {name}'.format(name=image_path))
             image_file = ImageFilenames()
             image_file.group_id = group_id
-            image_file.filename = str(filename)
+            # TODO: To path obect
+            image_file.filename = str(image_path)
             self.manager.save_object(image_file)
             self.main_window.increment_progress_bar()
-        if reload_list and images_list:
+        if reload_list and image_paths:
             self.load_full_list(self.manager.get_all_objects(ImageFilenames, order_by_ref=ImageFilenames.filename))
 
     def dnd_move_internal(self, target):
@@ -600,9 +614,10 @@
             return False
         # Continue with the existing images.
         for image in images:
-            name = os.path.split(image.filename)[1]
-            thumbnail = self.generate_thumbnail_path(image)
-            service_item.add_from_image(image.filename, name, background, thumbnail)
+            # TODO: To path object
+            name = Path(image.filename).name
+            thumbnail_path = self.generate_thumbnail_path(image)
+            service_item.add_from_image(Path(image.filename), name, background, thumbnail_path)
         return True
 
     def check_group_exists(self, new_group):

=== modified file 'openlp/plugins/media/lib/mediaitem.py'
--- openlp/plugins/media/lib/mediaitem.py	2016-12-31 11:01:36 +0000
+++ openlp/plugins/media/lib/mediaitem.py	2017-06-19 07:33:18 +0000
@@ -22,6 +22,7 @@
 
 import logging
 import os
+from pathlib2 import Path
 
 from PyQt5 import QtCore, QtWidgets
 
@@ -215,13 +216,12 @@
                                translate('MediaPlugin.MediaItem',
                                          'You must select a media file to replace the background with.')):
             item = self.list_view.currentItem()
-            filename = item.data(QtCore.Qt.UserRole)
-            if os.path.exists(filename):
+            file_path = item.data(QtCore.Qt.UserRole)
+            if file_path.exists():
                 service_item = ServiceItem()
                 service_item.title = 'webkit'
                 service_item.processor = 'webkit'
-                (path, name) = os.path.split(filename)
-                service_item.add_from_command(path, name, CLAPPERBOARD)
+                service_item.add_from_command(file_path.parent, file_path.name, CLAPPERBOARD)
                 if self.media_controller.video(DisplayControllerType.Live, service_item, video_behind_text=True):
                     self.reset_action.setVisible(True)
                     self.reset_action_context.setVisible(True)
@@ -232,8 +232,8 @@
             else:
                 critical_error_message_box(UiStrings().LiveBGError,
                                            translate('MediaPlugin.MediaItem',
-                                                     'There was a problem replacing your background, '
-                                                     'the media file "{name}" no longer exists.').format(name=filename))
+                                                     'There was a problem replacing your background, the media file '
+                                                     '"{name}" no longer exists.').format(name=file_path))
 
     def generate_slide_data(self, service_item, item=None, xml_version=False, remote=False,
                             context=ServiceItemContext.Service):
@@ -250,10 +250,24 @@
             item = self.list_view.currentItem()
             if item is None:
                 return False
-        filename = item.data(QtCore.Qt.UserRole)
-        # Special handling if the filename is a optical clip
-        if filename.startswith('optical:'):
-            (name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(filename)
+        item_data = item.data(QtCore.Qt.UserRole)
+        if isinstance(item_data, Path):
+            item_path = item_data
+            if not item_path.exists():
+                if not remote:
+                    # File is no longer present
+                    critical_error_message_box(
+                        translate('MediaPlugin.MediaItem', 'Missing Media File'),
+                        translate('MediaPlugin.MediaItem', 'The file {name} no longer exists.').format(name=item_path))
+                return False
+            service_item.title = item_path.name
+            service_item.processor = self.display_type_combo_box.currentText()
+            service_item.add_from_command(item_path.parent, item_path.name, CLAPPERBOARD)
+            # Only get start and end times if going to a service
+            if not self.media_controller.media_length(service_item):
+                return False
+        elif item_data.startswith('optical:'):
+            (name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(item_data)
             if not os.path.exists(name):
                 if not remote:
                     # Optical disc is no longer present
@@ -263,7 +277,7 @@
                                   'The optical disc {name} is no longer available.').format(name=name))
                 return False
             service_item.processor = self.display_type_combo_box.currentText()
-            service_item.add_from_command(filename, name, CLAPPERBOARD)
+            service_item.add_from_command(item_data, name, CLAPPERBOARD)
             service_item.title = clip_name
             # Set the length
             self.media_controller.media_setup_optical(name, title, audio_track, subtitle_track, start, end, None, None)
@@ -271,21 +285,6 @@
             service_item.start_time = start / 1000
             service_item.end_time = end / 1000
             service_item.add_capability(ItemCapabilities.IsOptical)
-        else:
-            if not os.path.exists(filename):
-                if not remote:
-                    # File is no longer present
-                    critical_error_message_box(
-                        translate('MediaPlugin.MediaItem', 'Missing Media File'),
-                        translate('MediaPlugin.MediaItem', 'The file {name} no longer exists.').format(name=filename))
-                return False
-            (path, name) = os.path.split(filename)
-            service_item.title = name
-            service_item.processor = self.display_type_combo_box.currentText()
-            service_item.add_from_command(path, name, CLAPPERBOARD)
-            # Only get start and end times if going to a service
-            if not self.media_controller.media_length(service_item):
-                return False
         service_item.add_capability(ItemCapabilities.CanAutoStartForLive)
         service_item.add_capability(ItemCapabilities.CanEditTitle)
         service_item.add_capability(ItemCapabilities.RequiresMedia)
@@ -300,7 +299,7 @@
         Initialize media item.
         """
         self.list_view.clear()
-        self.service_path = os.path.join(AppLocation.get_section_data_path(self.settings_section), 'thumbnails')
+        self.service_path = AppLocation.get_section_data_path(self.settings_section) / 'thumbnails'
         check_directory_exists(self.service_path)
         self.load_list(Settings().value(self.settings_section + '/media files'))
         self.rebuild_players()
@@ -350,6 +349,7 @@
             row_list.sort(reverse=True)
             for row in row_list:
                 self.list_view.takeItem(row)
+            # TODO: Path objects
             Settings().setValue(self.settings_section + '/media files', self.get_file_list())
 
     def load_list(self, media, target_group=None):
@@ -361,35 +361,37 @@
         """
         media.sort(key=lambda file_name: get_locale_key(os.path.split(str(file_name))[1]))
         for track in media:
-            track_info = QtCore.QFileInfo(track)
             item_name = None
-            if track.startswith('optical:'):
-                # Handle optical based item
-                (file_name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(track)
-                item_name = QtWidgets.QListWidgetItem(clip_name)
-                item_name.setIcon(self.optical_icon)
-                item_name.setData(QtCore.Qt.UserRole, track)
-                item_name.setToolTip('{name}@{start}-{end}'.format(name=file_name,
-                                                                   start=format_milliseconds(start),
-                                                                   end=format_milliseconds(end)))
-            elif not os.path.exists(track):
-                # File doesn't exist, mark as error.
-                file_name = os.path.split(str(track))[1]
-                item_name = QtWidgets.QListWidgetItem(file_name)
-                item_name.setIcon(self.error_icon)
-                item_name.setData(QtCore.Qt.UserRole, track)
-                item_name.setToolTip(track)
-            elif track_info.isFile():
-                # Normal media file handling.
-                file_name = os.path.split(str(track))[1]
-                item_name = QtWidgets.QListWidgetItem(file_name)
-                search = file_name.split('.')[-1].lower()
-                if '*.{text}'.format(text=search) in self.media_controller.audio_extensions_list:
-                    item_name.setIcon(self.audio_icon)
-                else:
-                    item_name.setIcon(self.video_icon)
-                item_name.setData(QtCore.Qt.UserRole, track)
-                item_name.setToolTip(track)
+            if isinstance(track, Path):
+                media_path = track
+                if not media_path.exists():
+                    # File doesn't exist, mark as error.
+                    file_name = media_path.name
+                    item_name = QtWidgets.QListWidgetItem(file_name)
+                    item_name.setIcon(self.error_icon)
+                    item_name.setData(QtCore.Qt.UserRole, media_path)
+                    item_name.setToolTip(str(media_path))
+                elif media_path.isFile():
+                    # Normal media file handling.
+                    file_name = media_path.name
+                    item_name = QtWidgets.QListWidgetItem(file_name)
+                    search = media_path.suffix.lower()
+                    if '*{text}'.format(text=search) in self.media_controller.audio_extensions_list:
+                        item_name.setIcon(self.audio_icon)
+                    else:
+                        item_name.setIcon(self.video_icon)
+                    item_name.setData(QtCore.Qt.UserRole, media_path)
+                    item_name.setToolTip(str(media_path))
+            else:
+                if track.startswith('optical:'):
+                    # Handle optical based item
+                    (file_name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(track)
+                    item_name = QtWidgets.QListWidgetItem(clip_name)
+                    item_name.setIcon(self.optical_icon)
+                    item_name.setData(QtCore.Qt.UserRole, track)
+                    item_name.setToolTip('{name}@{start}-{end}'.format(name=file_name,
+                                                                       start=format_milliseconds(start),
+                                                                       end=format_milliseconds(end)))
             if item_name:
                 self.list_view.addItem(item_name)
 
@@ -418,6 +420,7 @@
         :param show_error: Should the error be shown (True)
         :return: The search result.
         """
+        # TODO: to path objects
         files = Settings().value(self.settings_section + '/media files')
         results = []
         string = string.lower()

=== modified file 'openlp/plugins/media/lib/mediatab.py'
--- openlp/plugins/media/lib/mediatab.py	2016-12-31 11:01:36 +0000
+++ openlp/plugins/media/lib/mediatab.py	2017-06-19 07:33:18 +0000
@@ -30,10 +30,6 @@
     """
     MediaTab is the Media settings tab in the settings dialog.
     """
-    def __init__(self, parent, title, visible_title, icon_path):
-        self.parent = parent
-        super(MediaTab, self).__init__(parent, title, visible_title, icon_path)
-
     def setupUi(self):
         self.setObjectName('MediaTab')
         super(MediaTab, self).setupUi()

=== modified file 'openlp/plugins/media/mediaplugin.py'
--- openlp/plugins/media/mediaplugin.py	2016-12-31 11:01:36 +0000
+++ openlp/plugins/media/mediaplugin.py	2017-06-19 07:33:18 +0000
@@ -24,13 +24,12 @@
 """
 
 import logging
-import os
 import re
-from shutil import which
+from pathlib2 import Path
 
 from PyQt5 import QtCore
 
-from openlp.core.common import AppLocation, Settings, translate, check_binary_exists, is_win
+from openlp.core.common import AppLocation, translate, check_binary_exists
 from openlp.core.lib import Plugin, StringContent, build_icon
 from openlp.plugins.media.lib import MediaMediaItem, MediaTab
 
@@ -54,6 +53,7 @@
     def __init__(self):
         super(MediaPlugin, self).__init__('media', __default_settings__, MediaMediaItem)
         self.weight = -6
+        # TODO: icon_path to Path object?
         self.icon_path = ':/plugins/plugin_media.png'
         self.icon = build_icon(self.icon_path)
         # passed with drag and drop messages
@@ -72,10 +72,10 @@
         """
         log.debug('check_installed Mediainfo')
         # Try to find mediainfo in the path
-        exists = process_check_binary('mediainfo')
+        exists = process_check_binary(Path('mediainfo'))
         # If mediainfo is not in the path, try to find it in the application folder
         if not exists:
-            exists = process_check_binary(os.path.join(AppLocation.get_directory(AppLocation.AppDir), 'mediainfo'))
+            exists = process_check_binary(AppLocation.get_directory(AppLocation.AppDir) / 'mediainfo')
         return exists
 
     def app_startup(self):
@@ -160,9 +160,11 @@
     Function that checks whether a binary MediaInfo is present
 
     :param program_path:The full path to the binary to check.
+    :type program_path: Path
+    
     :return: If exists or not
+    :rtype: bool
     """
-    program_type = None
     runlog = check_binary_exists(program_path)
     # Analyse the output to see it the program is mediainfo
     for line in runlog.splitlines():

=== modified file 'openlp/plugins/presentations/lib/impresscontroller.py'
--- openlp/plugins/presentations/lib/impresscontroller.py	2017-05-14 10:11:10 +0000
+++ openlp/plugins/presentations/lib/impresscontroller.py	2017-06-19 07:33:18 +0000
@@ -34,6 +34,7 @@
 import logging
 import os
 import time
+from pathlib2 import Path
 
 from openlp.core.common import is_win, Registry, get_uno_command, get_uno_instance, delete_file
 
@@ -202,12 +203,18 @@
     Class which holds information and controls a single presentation.
     """
 
-    def __init__(self, controller, presentation):
+    def __init__(self, controller, document_path):
         """
         Constructor, store information about the file and initialise.
+
+        :param document_path: Path to the document to load
+        :type document_path: Path
+
+        :return: None
+        :rtype: None
         """
         log.debug('Init Presentation OpenOffice')
-        super(ImpressDocument, self).__init__(controller, presentation)
+        super(ImpressDocument, self).__init__(controller, document_path)
         self.document = None
         self.presentation = None
         self.control = None
@@ -224,10 +231,12 @@
             if desktop is None:
                 self.controller.start_process()
                 desktop = self.controller.get_com_desktop()
-            url = 'file:///' + self.file_path.replace('\\', '/').replace(':', '|').replace(' ', '%20')
+            # TODO: Check that I havnt broken this by removing replace(':', '|') as Path().as_uri() encodes ':'
+            #       as an html entity can check this by checking what the else statment returns
+            url = self.file_path.as_uri()
         else:
             desktop = self.controller.get_uno_desktop()
-            url = uno.systemPathToFileUrl(self.file_path)
+            url = uno.systemPathToFileUrl(str(self.file_path))
         if desktop is None:
             return False
         self.desktop = desktop
@@ -253,11 +262,7 @@
         log.debug('create thumbnails OpenOffice')
         if self.check_thumbnails():
             return
-        if is_win():
-            thumb_dir_url = 'file:///' + self.get_temp_folder().replace('\\', '/') \
-                .replace(':', '|').replace(' ', '%20')
-        else:
-            thumb_dir_url = uno.systemPathToFileUrl(self.get_temp_folder())
+        thumb_dir_url = self.get_temp_folder().as_uri()
         properties = []
         properties.append(self.create_property('FilterName', 'impress_png_Export'))
         properties = tuple(properties)
@@ -265,13 +270,13 @@
         pages = doc.getDrawPages()
         if not pages:
             return
-        if not os.path.isdir(self.get_temp_folder()):
-            os.makedirs(self.get_temp_folder())
+        if not self.get_temp_folder().is_dir():
+            self.get_temp_folder().mkdir(parents=True)
         for index in range(pages.getCount()):
             page = pages.getByIndex(index)
             doc.getCurrentController().setCurrentPage(page)
-            url_path = '{path}/{name}.png'.format(path=thumb_dir_url, name=str(index + 1))
-            path = os.path.join(self.get_temp_folder(), str(index + 1) + '.png')
+            url_path = '{path}/{name:d}.png'.format(path=thumb_dir_url, name=index + 1)
+            path = self.get_temp_folder() / '{slide_no:d}.png'.format(slide_no=index + 1)
             try:
                 doc.storeToURL(url_path, properties)
                 self.convert_thumbnail(path, index + 1)

=== modified file 'openlp/plugins/presentations/lib/mediaitem.py'
--- openlp/plugins/presentations/lib/mediaitem.py	2016-12-31 11:01:36 +0000
+++ openlp/plugins/presentations/lib/mediaitem.py	2017-06-19 07:33:18 +0000
@@ -22,6 +22,7 @@
 
 import logging
 import os
+from pathlib2 import Path
 
 from PyQt5 import QtCore, QtGui, QtWidgets
 
@@ -126,6 +127,7 @@
         Populate the media manager tab
         """
         self.list_view.setIconSize(QtCore.QSize(88, 50))
+        # TODO: Files to path objects?
         files = Settings().value(self.settings_section + '/presentations files')
         self.load_list(files, initial_load=True)
         self.populate_display_types()
@@ -156,49 +158,51 @@
         Add presentations into the media manager. This is called both on initial load of the plugin to populate with
         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]
+        # TODO: To files to path objects
+        file_paths = [Path(file) for file in files]
+        current_paths = self.get_file_list()
+        titles = [path.name for path in current_paths]
         self.application.set_busy_cursor()
         if not initial_load:
-            self.main_window.display_progress_bar(len(files))
+            self.main_window.display_progress_bar(len(file_paths))
         # Sort the presentations by its filename considering language specific characters.
-        files.sort(key=lambda filename: get_locale_key(os.path.split(str(filename))[1]))
-        for file in files:
+        file_paths.sort(key=lambda file_path: get_locale_key(file_path.name))
+        for file_path in file_paths:
             if not initial_load:
                 self.main_window.increment_progress_bar()
-            if current_list.count(file) > 0:
+            if current_paths.count(file_path) > 0:
                 continue
-            filename = os.path.split(file)[1]
-            if not os.path.exists(file):
-                item_name = QtWidgets.QListWidgetItem(filename)
+            file_name = file_path.name
+            if not file_path.exists():
+                item_name = QtWidgets.QListWidgetItem(file_name)
                 item_name.setIcon(build_icon(ERROR_IMAGE))
-                item_name.setData(QtCore.Qt.UserRole, file)
-                item_name.setToolTip(file)
+                item_name.setData(QtCore.Qt.UserRole, file_path)
+                item_name.setToolTip(str(file_path))
                 self.list_view.addItem(item_name)
             else:
-                if titles.count(filename) > 0:
+                if titles.count(file_name) > 0:
                     if not initial_load:
                         critical_error_message_box(translate('PresentationPlugin.MediaItem', 'File Exists'),
                                                    translate('PresentationPlugin.MediaItem',
                                                              'A presentation with that filename already exists.'))
                     continue
-                controller_name = self.find_controller_by_type(filename)
+                controller_name = self.find_controller_by_type(file_name)
                 if controller_name:
                     controller = self.controllers[controller_name]
-                    doc = controller.add_document(file)
-                    thumb = os.path.join(doc.get_thumbnail_folder(), 'icon.png')
-                    preview = doc.get_thumbnail_path(1, True)
-                    if not preview and not initial_load:
+                    doc = controller.add_document(file_path)
+                    thumbnail_path = doc.get_thumbnail_folder() / 'icon.png'
+                    preview_path = doc.get_thumbnail_path(1, True)
+                    if not preview_path and not initial_load:
                         doc.load_presentation()
-                        preview = doc.get_thumbnail_path(1, True)
+                        preview_path = doc.get_thumbnail_path(1, True)
                     doc.close_presentation()
-                    if not (preview and os.path.exists(preview)):
+                    if not (preview_path and preview_path.exists()):
                         icon = build_icon(':/general/general_delete.png')
                     else:
-                        if validate_thumb(preview, thumb):
-                            icon = build_icon(thumb)
+                        if validate_thumb(preview_path, thumbnail_path):
+                            icon = build_icon(thumbnail_path)
                         else:
-                            icon = create_thumb(preview, thumb)
+                            icon = create_thumb(preview_path, thumbnail_path)
                 else:
                     if initial_load:
                         icon = build_icon(':/general/general_delete.png')
@@ -207,10 +211,10 @@
                                                    translate('PresentationPlugin.MediaItem',
                                                              'This type of presentation is not supported.'))
                         continue
-                item_name = QtWidgets.QListWidgetItem(filename)
-                item_name.setData(QtCore.Qt.UserRole, file)
+                item_name = QtWidgets.QListWidgetItem(file_name)
+                item_name.setData(QtCore.Qt.UserRole, file_path)
                 item_name.setIcon(icon)
-                item_name.setToolTip(file)
+                item_name.setToolTip(str(file_path))
                 self.list_view.addItem(item_name)
         if not initial_load:
             self.main_window.finished_progress_bar()
@@ -227,8 +231,8 @@
             self.application.set_busy_cursor()
             self.main_window.display_progress_bar(len(row_list))
             for item in items:
-                filepath = str(item.data(QtCore.Qt.UserRole))
-                self.clean_up_thumbnails(filepath)
+                file_path = item.data(QtCore.Qt.UserRole)
+                self.clean_up_thumbnails(file_path)
                 self.main_window.increment_progress_bar()
             self.main_window.finished_progress_bar()
             for row in row_list:
@@ -236,23 +240,27 @@
             Settings().setValue(self.settings_section + '/presentations files', self.get_file_list())
             self.application.set_normal_cursor()
 
-    def clean_up_thumbnails(self, filepath, clean_for_update=False):
+    def clean_up_thumbnails(self, file_path, clean_for_update=False):
         """
         Clean up the files created such as thumbnails
 
-        :param filepath: File path of the presention to clean up after
+        :param file_path: File path of the presention to clean up after
+        :type file_path: Path
+
         :param clean_for_update: Only clean thumbnails if update is needed
+        :type clean_for_update: bool
+
         :return: None
+        :rtype: None
         """
         for cidx in self.controllers:
-            root, file_ext = os.path.splitext(filepath)
-            file_ext = file_ext[1:]
+            file_ext = file_path.suffix[1:]
             if file_ext in self.controllers[cidx].supports or file_ext in self.controllers[cidx].also_supports:
-                doc = self.controllers[cidx].add_document(filepath)
+                doc = self.controllers[cidx].add_document(file_path)
                 if clean_for_update:
                     thumb_path = doc.get_thumbnail_path(1, True)
-                    if not thumb_path or not os.path.exists(filepath) or os.path.getmtime(
-                            thumb_path) < os.path.getmtime(filepath):
+                    if not thumb_path or not file_path.exists() or \
+                            thumb_path.stat().st_mtime < file_path.stat().st_mtime:
                         doc.presentation_deleted()
                 else:
                     doc.presentation_deleted()
@@ -275,10 +283,11 @@
             items = self.list_view.selectedItems()
             if len(items) > 1:
                 return False
-        filename = presentation_file
-        if filename is None:
-            filename = items[0].data(QtCore.Qt.UserRole)
-        file_type = os.path.splitext(filename.lower())[1][1:]
+        # TODO: presentation_file to Path
+        file_path = Path(presentation_file)
+        if file_path is None:
+            file_path = items[0].data(QtCore.Qt.UserRole)
+        file_type = file_path.suffix.lower()[1:]
         if not self.display_type_combo_box.currentText():
             return False
         service_item.add_capability(ItemCapabilities.CanEditTitle)
@@ -291,29 +300,29 @@
             # force a nonexistent theme
             service_item.theme = -1
             for bitem in items:
-                filename = presentation_file
-                if filename is None:
-                    filename = bitem.data(QtCore.Qt.UserRole)
-                (path, name) = os.path.split(filename)
+                file_path = presentation_file
+                if file_path is None:
+                    file_path = bitem.data(QtCore.Qt.UserRole)
+                path, name = file_path.parent, file_path.name
                 service_item.title = name
-                if os.path.exists(filename):
-                    processor = self.find_controller_by_type(filename)
+                if file_path.exists():
+                    processor = self.find_controller_by_type(file_path)
                     if not processor:
                         return False
                     controller = self.controllers[processor]
                     service_item.processor = None
-                    doc = controller.add_document(filename)
-                    if doc.get_thumbnail_path(1, True) is None or not os.path.isfile(
-                            os.path.join(doc.get_temp_folder(), 'mainslide001.png')):
+                    doc = controller.add_document(str(file_path))
+                    if doc.get_thumbnail_path(1, True) is None or \
+                            not (doc.get_temp_folder() / 'mainslide001.png').is_file():
                         doc.load_presentation()
                     i = 1
-                    image = os.path.join(doc.get_temp_folder(), 'mainslide{number:0>3d}.png'.format(number=i))
-                    thumbnail = os.path.join(doc.get_thumbnail_folder(), 'slide%d.png' % i)
-                    while os.path.isfile(image):
-                        service_item.add_from_image(image, name, thumbnail=thumbnail)
+                    image_path = doc.get_temp_folder() / 'mainslide{number:0>3d}.png'.format(number=i)
+                    thumbnail_path = doc.get_thumbnail_folder() / 'slide%d.png' % i
+                    while image_path.is_file():
+                        service_item.add_from_image(image_path, name, thumbnail=str(thumbnail_path))
                         i += 1
-                        image = os.path.join(doc.get_temp_folder(), 'mainslide{number:0>3d}.png'.format(number=i))
-                        thumbnail = os.path.join(doc.get_thumbnail_folder(), 'slide{number:d}.png'.format(number=i))
+                        image_path = doc.get_temp_folder() / 'mainslide{number:0>3d}.png'.format(number=i)
+                        thumbnail_path = doc.get_thumbnail_folder() / 'slide{number:d}.png'.format(number=i)
                     service_item.add_capability(ItemCapabilities.HasThumbnails)
                     doc.close_presentation()
                     return True
@@ -323,34 +332,34 @@
                         critical_error_message_box(translate('PresentationPlugin.MediaItem', 'Missing Presentation'),
                                                    translate('PresentationPlugin.MediaItem',
                                                              'The presentation {name} no longer exists.'
-                                                             ).format(name=filename))
+                                                             ).format(name=file_path))
                     return False
         else:
             service_item.processor = self.display_type_combo_box.currentText()
             service_item.add_capability(ItemCapabilities.ProvidesOwnDisplay)
             for bitem in items:
-                filename = bitem.data(QtCore.Qt.UserRole)
-                (path, name) = os.path.split(filename)
+                file_path = bitem.data(QtCore.Qt.UserRole)
+                path, name = file_path.parent, file_path.name
                 service_item.title = name
-                if os.path.exists(filename):
+                if file_path.exists():
                     if self.display_type_combo_box.itemData(self.display_type_combo_box.currentIndex()) == 'automatic':
-                        service_item.processor = self.find_controller_by_type(filename)
+                        service_item.processor = self.find_controller_by_type(str(file_path))
                         if not service_item.processor:
                             return False
                     controller = self.controllers[service_item.processor]
-                    doc = controller.add_document(filename)
+                    doc = controller.add_document(file_path)
                     if doc.get_thumbnail_path(1, True) is None:
                         doc.load_presentation()
                     i = 1
-                    img = doc.get_thumbnail_path(i, True)
-                    if img:
+                    thumbnail_path = doc.get_thumbnail_path(i, True)
+                    if thumbnail_path:
                         # Get titles and notes
                         titles, notes = doc.get_titles_and_notes()
                         service_item.add_capability(ItemCapabilities.HasDisplayTitle)
                         if notes.count('') != len(notes):
                             service_item.add_capability(ItemCapabilities.HasNotes)
                         service_item.add_capability(ItemCapabilities.HasThumbnails)
-                        while img:
+                        while thumbnail_path:
                             # Use title and note if available
                             title = ''
                             if titles and len(titles) >= i:
@@ -358,9 +367,9 @@
                             note = ''
                             if notes and len(notes) >= i:
                                 note = notes[i - 1]
-                            service_item.add_from_command(path, name, img, title, note)
+                            service_item.add_from_command(path, name, str(thumbnail_path), title, note)
                             i += 1
-                            img = doc.get_thumbnail_path(i, True)
+                            thumbnail_path = doc.get_thumbnail_path(i, True)
                         doc.close_presentation()
                         return True
                     else:
@@ -370,7 +379,7 @@
                                                                  'Missing Presentation'),
                                                        translate('PresentationPlugin.MediaItem',
                                                                  'The presentation {name} is incomplete, '
-                                                                 'please reload.').format(name=filename))
+                                                                 'please reload.').format(name=file_path))
                         return False
                 else:
                     # File is no longer present
@@ -378,7 +387,7 @@
                         critical_error_message_box(translate('PresentationPlugin.MediaItem', 'Missing Presentation'),
                                                    translate('PresentationPlugin.MediaItem',
                                                              'The presentation {name} no longer exists.'
-                                                             ).format(name=filename))
+                                                             ).format(name=file_path))
                     return False
 
     def find_controller_by_type(self, filename):
@@ -389,6 +398,7 @@
 
         :param filename: The file name
         """
+        # TODO: Accept path object
         file_type = os.path.splitext(filename)[1][1:]
         if not file_type:
             return None

=== modified file 'openlp/plugins/presentations/lib/messagelistener.py'
--- openlp/plugins/presentations/lib/messagelistener.py	2016-12-31 11:01:36 +0000
+++ openlp/plugins/presentations/lib/messagelistener.py	2017-06-19 07:33:18 +0000
@@ -325,7 +325,7 @@
         is_live = message[1]
         item = message[0]
         hide_mode = message[2]
-        file = item.get_frame_path()
+        file = str(item.get_frame_path())
         self.handler = item.processor
         # When starting presentation from the servicemanager we convert
         # PDF/XPS/OXPS-serviceitems into image-serviceitems. When started from the mediamanager

=== modified file 'openlp/plugins/presentations/lib/pdfcontroller.py'
--- openlp/plugins/presentations/lib/pdfcontroller.py	2017-05-30 18:50:39 +0000
+++ openlp/plugins/presentations/lib/pdfcontroller.py	2017-06-19 07:33:18 +0000
@@ -23,6 +23,7 @@
 import os
 import logging
 import re
+from pathlib2 import Path
 from shutil import which
 from subprocess import check_output, CalledProcessError
 
@@ -65,8 +66,11 @@
         Function that checks whether a binary is either ghostscript or mudraw or neither.
         Is also used from presentationtab.py
 
-        :param program_path:The full path to the binary to check.
+        :param program_path: The full path to the binary to check.
+        :type program_path: Path
+        
         :return: Type of the binary, 'gs' if ghostscript, 'mudraw' if mudraw, None if invalid.
+        :type: str, None
         """
         program_type = None
         runlog = check_binary_exists(program_path)
@@ -106,13 +110,13 @@
         :return: True if program to open PDF-files was found, otherwise False.
         """
         log.debug('check_installed Pdf')
-        self.mudrawbin = ''
-        self.mutoolbin = ''
-        self.gsbin = ''
+        self.mudrawbin = None
+        self.mutoolbin = None
+        self.gsbin = None
         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(Settings().value('presentations/pdf_program'))
             program_type = self.process_check_binary(pdf_program)
             if program_type == 'gs':
                 self.gsbin = pdf_program
@@ -125,28 +129,34 @@
             application_path = AppLocation.get_directory(AppLocation.AppDir)
             if is_win():
                 # for windows we only accept mudraw.exe or mutool.exe in the base folder
-                application_path = AppLocation.get_directory(AppLocation.AppDir)
-                if os.path.isfile(os.path.join(application_path, 'mudraw.exe')):
-                    self.mudrawbin = os.path.join(application_path, 'mudraw.exe')
-                elif os.path.isfile(os.path.join(application_path, 'mutool.exe')):
-                    self.mutoolbin = os.path.join(application_path, 'mutool.exe')
+                if (application_path / 'mudraw.exe').is_file():
+                    self.mudrawbin = application_path / 'mudraw.exe'
+                elif (application_path, 'mutool.exe').is_file():
+                    self.mutoolbin = application_path / 'mutool.exe'
             else:
                 DEVNULL = open(os.devnull, 'wb')
                 # First try to find mudraw
-                self.mudrawbin = which('mudraw')
+                mudrawbin = which('mudraw')
+                if mudrawbin:
+                    self.mudrawbin = Path(mudrawbin)
                 # if mudraw isn't installed, try mutool
-                if not self.mudrawbin:
-                    self.mutoolbin = which('mutool')
-                    # Check we got a working mutool
-                    if not self.mutoolbin or self.process_check_binary(self.mutoolbin) != 'mutool':
-                        self.gsbin = which('gs')
+                else:
+                    mutoolbin = which('mutool')
+                    if mutoolbin:
+                        self.mutoolbin = Path(mutoolbin)
+                        # Check we got a working mutool
+                        if self.process_check_binary(self.mutoolbin) != 'mutool':
+                            gsbin = which('gs')
+                    else:
+                        gsbin = which('gs')
+                    if gsbin:
+                        self.gsbin = gsbin
                 # Last option: check if mudraw or mutool is placed in OpenLP base folder
                 if not self.mudrawbin and not self.mutoolbin and not self.gsbin:
-                    application_path = AppLocation.get_directory(AppLocation.AppDir)
-                    if os.path.isfile(os.path.join(application_path, 'mudraw')):
-                        self.mudrawbin = os.path.join(application_path, 'mudraw')
-                    elif os.path.isfile(os.path.join(application_path, 'mutool')):
-                        self.mutoolbin = os.path.join(application_path, 'mutool')
+                    if (application_path / 'mudraw').is_file():
+                        self.mudrawbin = application_path / 'mudraw'
+                    elif (application_path / 'mutool').is_file():
+                        self.mutoolbin = application_path / 'mutool'
         if self.mudrawbin or self.mutoolbin:
             self.also_supports = ['xps', 'oxps']
             return True
@@ -171,12 +181,18 @@
     image-serviceitem on the fly and present as such. Therefore some of the 'playback'
     functions is not implemented.
     """
-    def __init__(self, controller, presentation):
+    def __init__(self, controller, document_path):
         """
         Constructor, store information about the file and initialise.
+        
+        :param document_path: Path to the document to load
+        :type document_path: Path
+
+        :return: None
+        :rtype: None
         """
         log.debug('Init Presentation Pdf')
-        PresentationDocument.__init__(self, controller, presentation)
+        PresentationDocument.__init__(self, controller, document_path)
         self.presentation = None
         self.blanked = False
         self.hidden = False
@@ -200,12 +216,12 @@
         """
         # Use a postscript script to get size of the pdf. It is assumed that all pages have same size
         gs_resolution_script = AppLocation.get_directory(
-            AppLocation.PluginsDir) + '/presentations/lib/ghostscript_get_resolution.ps'
+            AppLocation.PluginsDir) / 'presentations' / 'lib' / 'ghostscript_get_resolution.ps'
         # Run the script on the pdf to get the size
         runlog = []
         try:
             runlog = check_output([self.controller.gsbin, '-dNOPAUSE', '-dNODISPLAY', '-dBATCH',
-                                   '-sFile=' + self.file_path, gs_resolution_script],
+                                   '-sFile=' + str(self.file_path), str(gs_resolution_script)],
                                   startupinfo=self.startupinfo)
         except CalledProcessError as e:
             log.debug(' '.join(e.cmd))
@@ -240,45 +256,48 @@
         """
         log.debug('load_presentation pdf')
         # Check if the images has already been created, and if yes load them
-        if os.path.isfile(os.path.join(self.get_temp_folder(), 'mainslide001.png')):
-            created_files = sorted(os.listdir(self.get_temp_folder()))
-            for fn in created_files:
-                if os.path.isfile(os.path.join(self.get_temp_folder(), fn)):
-                    self.image_files.append(os.path.join(self.get_temp_folder(), fn))
+        if (self.get_temp_folder() / 'mainslide001.png').is_file():
+            for file_path in self.get_temp_folder().iterdir():
+                if file_path.is_file():
+                    self.image_files.append(file_path)
+            self.image_files.sort()
             self.num_pages = len(self.image_files)
             return True
         size = ScreenList().current['size']
         # Generate images from PDF that will fit the frame.
         runlog = ''
         try:
-            if not os.path.isdir(self.get_temp_folder()):
-                os.makedirs(self.get_temp_folder())
+            if not self.get_temp_folder().is_dir():
+                self.get_temp_folder().mkdir(parents=True)
+
             # The %03d in the file name is handled by each binary
             if self.controller.mudrawbin:
                 log.debug('loading presentation using mudraw')
-                runlog = check_output([self.controller.mudrawbin, '-w', str(size.width()), '-h', str(size.height()),
-                                       '-o', os.path.join(self.get_temp_folder(), 'mainslide%03d.png'), self.file_path],
-                                      startupinfo=self.startupinfo)
+                runlog = check_output([str(self.controller.mudrawbin), '-w', str(size.width()),
+                                       '-h', str(size.height()),
+                                       '-o', str(self.get_temp_folder() / 'mainslide%03d.png'), str(self.file_path)],
+                                      startupinfo=self.startupinfo, universal_newlines=True)
             elif self.controller.mutoolbin:
                 log.debug('loading presentation using mutool')
-                runlog = check_output([self.controller.mutoolbin, 'draw', '-w', str(size.width()), '-h',
-                                       str(size.height()),
-                                       '-o', os.path.join(self.get_temp_folder(), 'mainslide%03d.png'), self.file_path],
-                                      startupinfo=self.startupinfo)
+                # TODO: Find out where the string convertsion actually happens
+                runlog = check_output([str(self.controller.mutoolbin), 'draw', '-w', str(size.width()),
+                                       '-h', str(size.height()),
+                                       '-o', str(self.get_temp_folder() / 'mainslide%03d.png'), str(self.file_path)],
+                                      startupinfo=self.startupinfo, universal_newlines=True)
             elif self.controller.gsbin:
                 log.debug('loading presentation using gs')
                 resolution = self.gs_get_resolution(size)
-                runlog = check_output([self.controller.gsbin, '-dSAFER', '-dNOPAUSE', '-dBATCH', '-sDEVICE=png16m',
+                runlog = check_output([str(self.controller.gsbin), '-dSAFER', '-dNOPAUSE', '-dBATCH', '-sDEVICE=png16m',
                                        '-r' + str(resolution), '-dTextAlphaBits=4', '-dGraphicsAlphaBits=4',
-                                       '-sOutputFile=' + os.path.join(self.get_temp_folder(), 'mainslide%03d.png'),
-                                       self.file_path], startupinfo=self.startupinfo)
-            created_files = sorted(os.listdir(self.get_temp_folder()))
-            for fn in created_files:
-                if os.path.isfile(os.path.join(self.get_temp_folder(), fn)):
-                    self.image_files.append(os.path.join(self.get_temp_folder(), fn))
-        except Exception as e:
-            log.debug(e)
-            log.debug(runlog)
+                                       '-sOutputFile=' + str(self.get_temp_folder() / 'mainslide%03d.png'),
+                                       str(self.file_path)],
+                                      startupinfo=self.startupinfo, universal_newlines=True)
+            for file_path in self.get_temp_folder().iterdir():
+                if file_path.is_file():
+                    self.image_files.append(file_path)
+            self.image_files.sort()
+        except Exception:
+            log.exception(runlog)
             return False
         self.num_pages = len(self.image_files)
         # Create thumbnails

=== modified file 'openlp/plugins/presentations/lib/powerpointcontroller.py'
--- openlp/plugins/presentations/lib/powerpointcontroller.py	2017-05-31 19:29:43 +0000
+++ openlp/plugins/presentations/lib/powerpointcontroller.py	2017-06-19 07:33:18 +0000
@@ -120,15 +120,20 @@
     Class which holds information and controls a single presentation.
     """
 
-    def __init__(self, controller, presentation):
+    def __init__(self, controller, document_path):
         """
         Constructor, store information about the file and initialise.
 
         :param controller:
-        :param presentation:
+
+        :param document_path: Path to the document to load
+        :type document_path: Path
+
+        :return: None
+        :rtype: None
         """
         log.debug('Init Presentation Powerpoint')
-        super(PowerpointDocument, self).__init__(controller, presentation)
+        super(PowerpointDocument, self).__init__(controller, document_path)
         self.presentation = None
         self.index_map = {}
         self.slide_count = 0
@@ -145,7 +150,8 @@
         try:
             if not self.controller.process:
                 self.controller.start_process()
-            self.controller.process.Presentations.Open(os.path.normpath(self.file_path), False, False, False)
+            # TODO: wheres open?
+            self.controller.process.Presentations.Open(os.path.normpath(str(self.file_path)), False, False, False)
             self.presentation = self.controller.process.Presentations(self.controller.process.Presentations.Count)
             self.create_thumbnails()
             self.create_titles_and_notes()
@@ -177,7 +183,7 @@
             if not self.presentation.Slides(num + 1).SlideShowTransition.Hidden:
                 self.index_map[key] = num + 1
                 self.presentation.Slides(num + 1).Export(
-                    os.path.join(self.get_thumbnail_folder(), 'slide{key:d}.png'.format(key=key)), 'png', 320, 240)
+                    self.get_thumbnail_folder() / 'slide{key:d}.png'.format(key=key), 'png', 320, 240)
                 key += 1
         self.slide_count = key - 1
 
@@ -363,9 +369,8 @@
                                                                           width=size.width(),
                                                                           horizontal=(right - left)))
         log.debug('window title: {title}'.format(title=window_title))
-        filename_root, filename_ext = os.path.splitext(os.path.basename(self.file_path))
         if size.y() == top and size.height() == (bottom - top) and size.x() == left and \
-                size.width() == (right - left) and filename_root in window_title:
+                size.width() == (right - left) and self.file_path.stem in window_title:
             log.debug('Found a match and will save the handle')
             self.presentation_hwnd = hwnd
             # Stop powerpoint from flashing in the taskbar

=== modified file 'openlp/plugins/presentations/lib/pptviewcontroller.py'
--- openlp/plugins/presentations/lib/pptviewcontroller.py	2017-05-14 10:11:10 +0000
+++ openlp/plugins/presentations/lib/pptviewcontroller.py	2017-06-19 07:33:18 +0000
@@ -85,9 +85,9 @@
             if self.process:
                 return
             log.debug('start PPTView')
-            dll_path = os.path.join(AppLocation.get_directory(AppLocation.AppDir),
-                                    'plugins', 'presentations', 'lib', 'pptviewlib', 'pptviewlib.dll')
-            self.process = cdll.LoadLibrary(dll_path)
+            dll_path = AppLocation.get_directory(AppLocation.AppDir) \
+                / 'plugins' / 'presentations' / 'lib' / 'pptviewlib' / 'pptviewlib.dll'
+            self.process = cdll.LoadLibrary(str(dll_path))
             if log.isEnabledFor(logging.DEBUG):
                 self.process.SetDebug(1)
 
@@ -104,12 +104,18 @@
     """
     Class which holds information and controls a single presentation.
     """
-    def __init__(self, controller, presentation):
+    def __init__(self, controller, document_path):
         """
         Constructor, store information about the file and initialise.
+
+        :param document_path: Path to the document to load
+        :type document_path: Path
+
+        :return: None
+        :rtype: None
         """
         log.debug('Init Presentation PowerPoint')
-        super(PptviewDocument, self).__init__(controller, presentation)
+        super(PptviewDocument, self).__init__(controller, document_path)
         self.presentation = None
         self.ppt_id = None
         self.blanked = False
@@ -121,7 +127,8 @@
         the background PptView task started earlier.
         """
         log.debug('LoadPresentation')
-        temp_folder = self.get_temp_folder()
+        # TODO: temp_folder to Path objectr
+        temp_folder = str(self.get_temp_folder())
         size = ScreenList().current['size']
         rect = RECT(size.x(), size.y(), size.right(), size.bottom())
         self.file_path = os.path.normpath(self.file_path)
@@ -148,7 +155,7 @@
             return
         log.debug('create_thumbnails proceeding')
         for idx in range(self.get_slide_count()):
-            path = '{folder}\\slide{index}.bmp'.format(folder=self.get_temp_folder(), index=str(idx + 1))
+            path = self.get_temp_folder() / 'slide{index}.bmp'.format(index=str(idx + 1))
             self.convert_thumbnail(path, idx + 1)
 
     def create_titles_and_notes(self):

=== modified file 'openlp/plugins/presentations/lib/presentationcontroller.py'
--- openlp/plugins/presentations/lib/presentationcontroller.py	2016-12-31 11:01:36 +0000
+++ openlp/plugins/presentations/lib/presentationcontroller.py	2017-06-19 07:33:18 +0000
@@ -22,8 +22,9 @@
 
 import logging
 import os
-import shutil
+from pathlib2 import Path
 
+from patches.shutilpatches import rmtree
 from PyQt5 import QtCore
 
 from openlp.core.common import Registry, AppLocation, Settings, check_directory_exists, md5_hash
@@ -85,19 +86,31 @@
         Returns a path to an image containing a preview for the requested slide
 
     """
-    def __init__(self, controller, name):
+    def __init__(self, controller, document_path):
         """
         Constructor for the PresentationController class
+        
+        :param document_path: Path to the document to load
+        :type document_path: Path
+
+        :return: None
+        :rtype: None
         """
         self.controller = controller
-        self._setup(name)
+        self._setup(document_path)
 
-    def _setup(self, name):
+    def _setup(self, document_path):
         """
         Run some initial setup. This method is separate from __init__ in order to mock it out in tests.
+
+        :param document_path: Path to the document to load
+        :type document_path: Path
+
+        :return: None
+        :rtype: None
         """
         self.slide_number = 0
-        self.file_path = name
+        self.file_path = document_path
         check_directory_exists(self.get_thumbnail_folder())
 
     def load_presentation(self):
@@ -115,49 +128,54 @@
         a file, e.g. thumbnails
         """
         try:
-            if os.path.exists(self.get_thumbnail_folder()):
-                shutil.rmtree(self.get_thumbnail_folder())
-            if os.path.exists(self.get_temp_folder()):
-                shutil.rmtree(self.get_temp_folder())
+            if self.get_thumbnail_folder().exists():
+                rmtree(self.get_thumbnail_folder())
+            if self.get_temp_folder().exists():
+                rmtree(self.get_temp_folder())
         except OSError:
             log.exception('Failed to delete presentation controller files')
 
-    def get_file_name(self):
-        """
-        Return just the filename of the presentation, without the directory
-        """
-        return os.path.split(self.file_path)[1]
-
     def get_thumbnail_folder(self):
         """
         The location where thumbnail images will be stored
+        
+        :return: The path to the thumbnail
+        :rtype: Path
         """
         # TODO: If statement can be removed when the upgrade path from 2.0.x to 2.2.x is no longer needed
         if Settings().value('presentations/thumbnail_scheme') == 'md5':
-            folder = md5_hash(self.file_path.encode('utf-8'))
+            folder = md5_hash(bytes(self.file_path))
         else:
-            folder = self.get_file_name()
-        return os.path.join(self.controller.thumbnail_folder, folder)
+            folder = self.file_path.name
+        # TODO: Find where self.controller.thumbnail_folder comes from
+        return Path(self.controller.thumbnail_folder / folder)
 
     def get_temp_folder(self):
         """
         The location where thumbnail images will be stored
+        
+        :return: The path to the temporary file folder
+        :rtype: Path
         """
         # TODO: If statement can be removed when the upgrade path from 2.0.x to 2.2.x is no longer needed
         if Settings().value('presentations/thumbnail_scheme') == 'md5':
-            folder = md5_hash(self.file_path.encode('utf-8'))
+            folder = md5_hash(bytes(self.file_path))
         else:
-            folder = folder = self.get_file_name()
-        return os.path.join(self.controller.temp_folder, folder)
+            folder = self.file_path.name
+        # TODO: Find where self.controller.temp_folder comes from
+        return Path(self.controller.temp_folder, folder)
 
     def check_thumbnails(self):
         """
-        Returns ``True`` if the thumbnail images exist and are more recent than the powerpoint file.
+        Check that the last thumbnail image exists and is valid and are more recent than the powerpoint file.
+        
+        :return: If the thumbnail is valid
+        :rtype: bool
         """
-        last_image = self.get_thumbnail_path(self.get_slide_count(), True)
-        if not (last_image and os.path.isfile(last_image)):
+        last_image_path = self.get_thumbnail_path(self.get_slide_count(), True)
+        if not (last_image_path and last_image_path.is_file()):
             return False
-        return validate_thumb(self.file_path, last_image)
+        return validate_thumb(self.file_path, last_image_path)
 
     def close_presentation(self):
         """
@@ -240,25 +258,40 @@
         """
         pass
 
-    def convert_thumbnail(self, file, idx):
+    def convert_thumbnail(self, image_path, index):
         """
         Convert the slide image the application made to a scaled 360px height .png image.
+        
+        :param image_path: Path to the image to create a thumb nail of
+        :type image_path: Path
+        
+        :param index: The index of the slide to create the thumbnail for.
+        :type index: int
+        
+        :return: None
+        :rtype: None        
         """
         if self.check_thumbnails():
             return
-        if os.path.isfile(file):
-            thumb_path = self.get_thumbnail_path(idx, False)
-            create_thumb(file, thumb_path, False, QtCore.QSize(-1, 360))
+        if image_path.is_file():
+            thumb_path = self.get_thumbnail_path(index, False)
+            create_thumb(image_path, thumb_path, False, QtCore.QSize(-1, 360))
 
-    def get_thumbnail_path(self, slide_no, check_exists):
+    def get_thumbnail_path(self, slide_no, check_exists=False):
         """
         Returns an image path containing a preview for the requested slide
 
         :param slide_no: The slide an image is required for, starting at 1
-        :param check_exists:
+        :type slide_no: int
+        
+        :param check_exists: Check if the generated path exists
+        :type check_exists: bo  
+        
+        :return: The path, or None if the :param:`check_exists` is True and the file does not exist
+        :rtype: Path, None
         """
-        path = os.path.join(self.get_thumbnail_folder(), self.controller.thumbnail_prefix + str(slide_no) + '.png')
-        if os.path.isfile(path) or not check_exists:
+        path = self.get_thumbnail_folder() / (self.controller.thumbnail_prefix + str(slide_no) + '.png')
+        if path.is_file() or not check_exists:
             return path
         else:
             return None
@@ -301,44 +334,43 @@
         Reads the titles from the titles file and
         the notes files and returns the content in two lists
         """
-        titles = []
         notes = []
-        titles_file = os.path.join(self.get_thumbnail_folder(), 'titles.txt')
-        if os.path.exists(titles_file):
+        titles_path = self.get_thumbnail_folder() / 'titles.txt'
+        try:
+            titles = titles_path.read_text().splitlines()
+        except:
+            log.exception('Failed to open/read existing titles file')
+            titles = []
+        for slide_no, title in enumerate(titles, 1):
+            notes_path = self.get_thumbnail_folder() / 'slideNotes{number:d}.txt'.format(number=slide_no)
             try:
-                with open(titles_file, encoding='utf-8') as fi:
-                    titles = fi.read().splitlines()
+                note = notes_path.read_text()
             except:
-                log.exception('Failed to open/read existing titles file')
-                titles = []
-        for slide_no, title in enumerate(titles, 1):
-            notes_file = os.path.join(self.get_thumbnail_folder(), 'slideNotes{number:d}.txt'.format(number=slide_no))
-            note = ''
-            if os.path.exists(notes_file):
-                try:
-                    with open(notes_file, encoding='utf-8') as fn:
-                        note = fn.read()
-                except:
-                    log.exception('Failed to open/read notes file')
-                    note = ''
+                log.exception('Failed to open/read notes file')
+                note = ''
             notes.append(note)
         return titles, notes
 
     def save_titles_and_notes(self, titles, notes):
         """
-        Performs the actual persisting of titles to the titles.txt
-        and notes to the slideNote%.txt
+        Performs the actual persisting of titles to the titles.txt and notes to the slideNote%.txt
+
+        :param titles: The titles to save
+        :type titles: list[str]
+
+        :param notes: The notes to save
+        :type notes: list[str]
+
+        :return: None
+        :rtype: None
         """
         if titles:
-            titles_file = os.path.join(self.get_thumbnail_folder(), 'titles.txt')
-            with open(titles_file, mode='wt', encoding='utf-8') as fo:
-                fo.writelines(titles)
+            titles_path = self.get_thumbnail_folder() / 'titles.txt'
+            titles_path.write_text('\n'.join(titles))
         if notes:
             for slide_no, note in enumerate(notes, 1):
-                notes_file = os.path.join(self.get_thumbnail_folder(),
-                                          'slideNotes{number:d}.txt'.format(number=slide_no))
-                with open(notes_file, mode='wt', encoding='utf-8') as fn:
-                    fn.write(note)
+                notes_path = self.get_thumbnail_folder() / 'slideNotes{number:d}.txt'.format(number=slide_no)
+                notes_path.write_text(note)
 
 
 class PresentationController(object):
@@ -415,8 +447,8 @@
         self.document_class = document_class
         self.settings_section = self.plugin.settings_section
         self.available = None
-        self.temp_folder = os.path.join(AppLocation.get_section_data_path(self.settings_section), name)
-        self.thumbnail_folder = os.path.join(AppLocation.get_section_data_path(self.settings_section), 'thumbnails')
+        self.temp_folder = AppLocation.get_section_data_path(self.settings_section) / name
+        self.thumbnail_folder = AppLocation.get_section_data_path(self.settings_section) / 'thumbnails'
         self.thumbnail_prefix = 'slide'
         check_directory_exists(self.thumbnail_folder)
         check_directory_exists(self.temp_folder)
@@ -454,11 +486,17 @@
         log.debug('Kill')
         self.close_presentation()
 
-    def add_document(self, name):
+    def add_document(self, document_path):
         """
         Called when a new presentation document is opened.
+
+        :param document_path: Path to the document to load
+        :type document_path: Path
+        
+        :return: The document
+        :rtype: PresentationDocument
         """
-        document = self.document_class(self, name)
+        document = self.document_class(self, document_path)
         self.docs.append(document)
         return document
 

=== 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-06-19 07:33:18 +0000
@@ -19,11 +19,11 @@
 # with this program; if not, write to the Free Software Foundation, Inc., 59  #
 # Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
 ###############################################################################
-
-from PyQt5 import QtGui, QtWidgets
+from patches.utils import path_to_str, str_to_path
+from PyQt5 import QtWidgets
 
 from openlp.core.common import Settings, UiStrings, translate
-from openlp.core.lib import SettingsTab, build_icon
+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
@@ -37,9 +37,8 @@
         """
         Constructor
         """
-        self.parent = parent
         self.controllers = controllers
-        super(PresentationTab, self).__init__(parent, title, visible_title, icon_path)
+        super().__init__(parent, title, visible_title, icon_path)
         self.activated = False
 
     def setupUi(self):
@@ -155,8 +154,7 @@
         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 = pdf_program
+        self.program_path_edit.path = str_to_path(pdf_program)
 
     def save(self):
         """
@@ -192,7 +190,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 +217,18 @@
             checkbox.setEnabled(controller.is_available())
             self.set_controller_text(checkbox, controller)
 
-    def on_program_path_edit_path_changed(self, filename):
-        """
-        Select the mudraw or ghostscript binary that should be used.
-        """
-        if filename:
-            if not PdfController.process_check_binary(filename):
+    def on_program_path_edit_path_changed(self, new_path):
+        """
+        Handle the `pathEditChanged` signal from program_path_edit
+        
+        :param new_path: Path to the new program
+        :type new_path: Path
+        
+        :return: None
+        :rtype: None
+        """
+        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/presentations/presentationplugin.py'
--- openlp/plugins/presentations/presentationplugin.py	2017-06-09 06:06:49 +0000
+++ openlp/plugins/presentations/presentationplugin.py	2017-06-19 07:33:18 +0000
@@ -64,6 +64,7 @@
         self.controllers = {}
         Plugin.__init__(self, 'presentations', __default_settings__, __default_settings__)
         self.weight = -8
+        # TODO: icon_path to Path object?
         self.icon_path = ':/plugins/plugin_presentations.png'
         self.icon = build_icon(self.icon_path)
 

=== modified file 'openlp/plugins/remotes/lib/httprouter.py'
--- openlp/plugins/remotes/lib/httprouter.py	2017-01-25 21:17:27 +0000
+++ openlp/plugins/remotes/lib/httprouter.py	2017-06-19 07:33:18 +0000
@@ -112,6 +112,7 @@
 import re
 import urllib.request
 import urllib.error
+from pathlib2 import Path
 from urllib.parse import urlparse, parse_qs
 
 from mako.template import Template
@@ -171,8 +172,10 @@
         ]
         self.settings_section = 'remotes'
         self.translate()
-        self.html_dir = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), 'remotes', 'html')
-        self.config_dir = os.path.join(AppLocation.get_data_path(), 'stages')
+        # TODO: self.html_dir to Path object
+        self.html_dir = str(AppLocation.get_directory(AppLocation.PluginsDir) / 'remotes' / 'html')
+        # TODO: self.config_dir to Path object
+        self.config_dir = str(AppLocation.get_data_path() / 'stages')
 
     def do_post_processor(self):
         """
@@ -454,15 +457,16 @@
         content_type = None
         if controller_name and file_name:
             if controller_name in supported_controllers:
+                # TODO: full_path to Path object
                 full_path = urllib.parse.unquote(file_name)
                 if '..' not in full_path:  # no hacking please
-                    full_path = os.path.normpath(os.path.join(AppLocation.get_section_data_path(controller_name),
-                                                              'thumbnails/' + full_path))
+                    full_path = os.path.normpath(
+                        str(AppLocation.get_section_data_path(controller_name) / 'thumbnails' / full_path))
                     if os.path.exists(full_path):
                         path, just_file_name = os.path.split(full_path)
                         self.image_manager.add_image(full_path, just_file_name, None, width, height)
                         ext, content_type = self.get_content_type(full_path)
-                        image = self.image_manager.get_image(full_path, just_file_name, width, height)
+                        image = self.image_manager.get_image(Path(full_path), just_file_name, width, height)
                         content = image_to_byte(image, False)
         if len(content) == 0:
             return self.do_not_found()
@@ -564,12 +568,12 @@
                 # Handle images, unless a custom thumbnail is given or if thumbnails is disabled
                 elif current_item.is_image() and not frame.get('image', '') and Settings().value('remotes/thumbnails'):
                     item['tag'] = str(index + 1)
-                    thumbnail_path = os.path.join('images', 'thumbnails', frame['title'])
-                    full_thumbnail_path = os.path.join(AppLocation.get_data_path(), thumbnail_path)
+                    thumbnail_path = Path('images', 'thumbnails', frame['title'])
+                    full_thumbnail_path = AppLocation.get_data_path() / thumbnail_path
                     # Create thumbnail if it doesn't exists
-                    if not os.path.exists(full_thumbnail_path):
+                    if not full_thumbnail_path.exists():
                         create_thumb(current_item.get_frame_path(index), full_thumbnail_path, False)
-                    item['img'] = urllib.request.pathname2url(os.path.sep + thumbnail_path)
+                    item['img'] = urllib.request.pathname2url(os.path.sep + str(thumbnail_path))
                     item['text'] = str(frame['title'])
                     item['html'] = str(frame['title'])
                 else:
@@ -582,7 +586,8 @@
                     if current_item.is_capable(ItemCapabilities.HasThumbnails) and \
                             Settings().value('remotes/thumbnails'):
                         # If the file is under our app directory tree send the portion after the match
-                        data_path = AppLocation.get_data_path()
+                        data_path = str(AppLocation.get_data_path())
+                        # TODO: Convert frame['image'] to Path object
                         if frame['image'][0:len(data_path)] == data_path:
                             item['img'] = urllib.request.pathname2url(frame['image'][len(data_path):])
                     item['text'] = str(frame['title'])

=== modified file 'openlp/plugins/remotes/lib/remotetab.py'
--- openlp/plugins/remotes/lib/remotetab.py	2017-03-15 19:51:10 +0000
+++ openlp/plugins/remotes/lib/remotetab.py	2017-06-19 07:33:18 +0000
@@ -34,9 +34,6 @@
     """
     RemoteTab is the Remotes settings tab in the settings dialog.
     """
-    def __init__(self, parent, title, visible_title, icon_path):
-        super(RemoteTab, self).__init__(parent, title, visible_title, icon_path)
-
     def setupUi(self):
         self.setObjectName('RemoteTab')
         super(RemoteTab, self).setupUi()

=== modified file 'openlp/plugins/remotes/remoteplugin.py'
--- openlp/plugins/remotes/remoteplugin.py	2017-05-30 18:42:35 +0000
+++ openlp/plugins/remotes/remoteplugin.py	2017-06-19 07:33:18 +0000
@@ -50,6 +50,7 @@
         remotes constructor
         """
         super(RemotesPlugin, self).__init__('remotes', __default_settings__, settings_tab_class=RemoteTab)
+        # TODO: icon_path to Path object?
         self.icon_path = ':/plugins/plugin_remote.png'
         self.icon = build_icon(self.icon_path)
         self.weight = -1

=== modified file 'openlp/plugins/songs/forms/editsongform.py'
--- openlp/plugins/songs/forms/editsongform.py	2017-06-09 06:06:49 +0000
+++ openlp/plugins/songs/forms/editsongform.py	2017-06-19 07:33:18 +0000
@@ -23,11 +23,12 @@
 The :mod:`~openlp.plugins.songs.forms.editsongform` module contains the form
 used to edit songs.
 """
-
 import logging
+import os
 import re
-import os
 import shutil
+from patches.utils import path_to_str, str_to_path
+from patches.pyqt5patches import PQFileDialog
 
 from PyQt5 import QtCore, QtWidgets
 
@@ -542,6 +543,7 @@
             self.add_songbook_entry_to_list(songbook_entry.songbook.id, songbook_entry.songbook.name,
                                             songbook_entry.entry)
         self.audio_list_widget.clear()
+        # TODO: Path objects
         for media in self.song.media_files:
             media_file = QtWidgets.QListWidgetItem(os.path.split(media.file_name)[1])
             media_file.setData(QtCore.Qt.UserRole, media.file_name)
@@ -925,11 +927,11 @@
         Loads file(s) from the filesystem.
         """
         filters = '{text} (*)'.format(text=UiStrings().AllFiles)
-        file_names = FileDialog.getOpenFileNames(self, translate('SongsPlugin.EditSongForm', 'Open File(s)'), '',
-                                                 filters)
-        for filename in file_names:
-            item = QtWidgets.QListWidgetItem(os.path.split(str(filename))[1])
-            item.setData(QtCore.Qt.UserRole, filename)
+        file_paths = PQFileDialog.getOpenFileNames(
+            parent=self, caption=translate('SongsPlugin.EditSongForm', 'Open File(s)'), filters=filters)
+        for file_path in file_paths:
+            item = QtWidgets.QListWidgetItem(file_path.name)
+            item.setData(QtCore.Qt.UserRole, file_path)
             self.audio_list_widget.addItem(item)
 
     def on_audio_add_from_media_button_clicked(self):
@@ -1065,16 +1067,16 @@
         self.manager.save_object(self.song)
         audio_files = [a.file_name for a in self.song.media_files]
         log.debug(audio_files)
-        save_path = os.path.join(AppLocation.get_section_data_path(self.media_item.plugin.name), 'audio',
-                                 str(self.song.id))
+        save_path = AppLocation.get_section_data_path(self.media_item.plugin.name) / 'audio' / str(self.song.id)
         check_directory_exists(save_path)
         self.song.media_files = []
         files = []
         for row in range(self.audio_list_widget.count()):
             item = self.audio_list_widget.item(row)
             filename = item.data(QtCore.Qt.UserRole)
-            if not filename.startswith(save_path):
-                old_file, filename = filename, os.path.join(save_path, os.path.split(filename)[1])
+            # TODO: Can startswith use the Path object
+            if not filename.startswith(str(save_path)):
+                old_file, filename = filename, str(save_path / os.path.split(filename)[1])
                 shutil.copyfile(old_file, filename)
             files.append(filename)
             media_file = MediaFile()
@@ -1090,7 +1092,7 @@
                     log.exception('Could not remove file: {audio}'.format(audio=audio))
         if not files:
             try:
-                os.rmdir(save_path)
+                save_path.rmdir()
             except OSError:
                 log.exception('Could not remove directory: {path}'.format(path=save_path))
         clean_song(self.manager, self.song)

=== modified file 'openlp/plugins/songs/forms/songexportform.py'
--- openlp/plugins/songs/forms/songexportform.py	2017-05-30 18:42:35 +0000
+++ openlp/plugins/songs/forms/songexportform.py	2017-06-19 07:33:18 +0000
@@ -25,11 +25,13 @@
 """
 import logging
 
+from patches.utils import path_to_str, str_to_path
 from PyQt5 import QtCore, QtWidgets
 
-from openlp.core.common import Registry, UiStrings, translate
-from openlp.core.lib import create_separated_list, build_icon
+from openlp.core.common import Registry, Settings, UiStrings, translate
+from openlp.core.lib import create_separated_list
 from openlp.core.lib.ui import critical_error_message_box
+from openlp.core.ui.lib import PathEdit, PathType
 from openlp.core.ui.lib.wizard import OpenLPWizard, WizardStrings
 from openlp.plugins.songs.lib.db import Song
 from openlp.plugins.songs.lib.openlyricsexport import OpenLyricsExport
@@ -76,7 +78,6 @@
         self.search_line_edit.textEdited.connect(self.on_search_line_edit_changed)
         self.uncheck_button.clicked.connect(self.on_uncheck_button_clicked)
         self.check_button.clicked.connect(self.on_check_button_clicked)
-        self.directory_button.clicked.connect(self.on_directory_button_clicked)
 
     def add_custom_pages(self):
         """
@@ -120,21 +121,17 @@
         self.grid_layout.setObjectName('range_layout')
         self.selected_list_widget = QtWidgets.QListWidget(self.export_song_page)
         self.selected_list_widget.setObjectName('selected_list_widget')
-        self.grid_layout.addWidget(self.selected_list_widget, 1, 0, 1, 1)
-        # FIXME: self.horizontal_layout is already defined above?!?!? Replace with Path Eidt!
-        self.horizontal_layout = QtWidgets.QHBoxLayout()
-        self.horizontal_layout.setObjectName('horizontal_layout')
+        self.grid_layout.addWidget(self.selected_list_widget, 1, 0, 1, 2)
+        self.output_directory_path_edit = PathEdit(
+            self.export_song_page, PathType.Directories,
+            dialog_caption=translate('SongsPlugin.ExportWizardForm', 'Select Destination Folder'), show_revert=False)
+        # TODO: To Path object
+        last_dir = Settings().value('songs/last directory export')
+        self.output_directory_path_edit.path = str_to_path(last_dir)
         self.directory_label = QtWidgets.QLabel(self.export_song_page)
         self.directory_label.setObjectName('directory_label')
-        self.horizontal_layout.addWidget(self.directory_label)
-        self.directory_line_edit = QtWidgets.QLineEdit(self.export_song_page)
-        self.directory_line_edit.setObjectName('directory_line_edit')
-        self.horizontal_layout.addWidget(self.directory_line_edit)
-        self.directory_button = QtWidgets.QToolButton(self.export_song_page)
-        self.directory_button.setIcon(build_icon(':/exports/export_load.png'))
-        self.directory_button.setObjectName('directory_button')
-        self.horizontal_layout.addWidget(self.directory_button)
-        self.grid_layout.addLayout(self.horizontal_layout, 0, 0, 1, 1)
+        self.grid_layout.addWidget(self.directory_label, 0, 0)
+        self.grid_layout.addWidget(self.output_directory_path_edit, 0, 1)
         self.export_song_layout.addLayout(self.grid_layout)
         self.addPage(self.export_song_page)
 
@@ -188,11 +185,12 @@
                 self.selected_list_widget.addItem(song)
             return True
         elif self.currentPage() == self.export_song_page:
-            if not self.directory_line_edit.text():
+            if not self.output_directory_path_edit.path:
                 critical_error_message_box(
                     translate('SongsPlugin.ExportWizardForm', 'No Save Location specified'),
                     translate('SongsPlugin.ExportWizardForm', 'You need to specify a directory.'))
                 return False
+            Settings().set_path_value('songs/last directory export', self.output_directory_path_edit.path)
             return True
         elif self.currentPage() == self.progress_page:
             self.available_list_widget.clear()
@@ -211,8 +209,8 @@
         self.finish_button.setVisible(False)
         self.cancel_button.setVisible(True)
         self.available_list_widget.clear()
-        self.selected_list_widget.clear()
-        self.directory_line_edit.clear()
+        # TODO: Work out where this is called from and if I need to implement clear in the path edit widget
+        # self.output_directory_path_edit.clear()
         self.search_line_edit.clear()
         # Load the list of songs.
         self.application.set_busy_cursor()
@@ -247,7 +245,7 @@
             song.data(QtCore.Qt.UserRole)
             for song in find_list_widget_items(self.selected_list_widget)
         ]
-        exporter = OpenLyricsExport(self, songs, self.directory_line_edit.text())
+        exporter = OpenLyricsExport(self, songs, self.output_directory_path_edit.path)
         try:
             if exporter.do_export():
                 self.progress_label.setText(
@@ -291,15 +289,6 @@
             if not item.isHidden():
                 item.setCheckState(QtCore.Qt.Checked)
 
-    def on_directory_button_clicked(self):
-        """
-        Called when the *directory_button* was clicked. Opens a dialog and writes
-        the path to *directory_line_edit*.
-        """
-        self.get_folder(
-            translate('SongsPlugin.ExportWizardForm', 'Select Destination Folder'),
-            self.directory_line_edit, 'last directory export')
-
 
 def find_list_widget_items(list_widget, text=''):
     """

=== modified file 'openlp/plugins/songs/forms/songimportform.py'
--- openlp/plugins/songs/forms/songimportform.py	2017-05-30 18:42:35 +0000
+++ openlp/plugins/songs/forms/songimportform.py	2017-06-19 07:33:18 +0000
@@ -25,15 +25,19 @@
 import codecs
 import logging
 import os
+from pathlib2 import Path
 
+from patches.utils import str_to_path
+from patches.pyqt5patches import PQFileDialog
 from PyQt5 import QtCore, QtWidgets
 
 from openlp.core.common import RegistryProperties, Settings, UiStrings, translate
-from openlp.core.lib import FileDialog
 from openlp.core.lib.ui import critical_error_message_box
+from openlp.core.ui.lib import PathEdit, PathType
 from openlp.core.ui.lib.wizard import OpenLPWizard, WizardStrings
 from openlp.plugins.songs.lib.importer import SongFormat, SongFormatSelect
 
+
 log = logging.getLogger(__name__)
 
 
@@ -92,9 +96,7 @@
                 self.format_widgets[song_format]['addButton'].clicked.connect(self.on_add_button_clicked)
                 self.format_widgets[song_format]['removeButton'].clicked.connect(self.on_remove_button_clicked)
             else:
-                self.format_widgets[song_format]['browseButton'].clicked.connect(self.on_browse_button_clicked)
-                self.format_widgets[song_format]['file_path_edit'].textChanged.\
-                    connect(self.on_filepath_edit_text_changed)
+                self.format_widgets[song_format]['path_edit'].pathChanged.connect(self.on_path_edit_path_changed)
 
     def add_custom_pages(self):
         """
@@ -154,7 +156,6 @@
                 self.format_widgets[format_list]['removeButton'].setText(
                     translate('SongsPlugin.ImportWizardForm', 'Remove File(s)'))
             else:
-                self.format_widgets[format_list]['browseButton'].setText(UiStrings().Browse)
                 f_label = 'Filename:'
                 if select_mode == SongFormatSelect.SingleFolder:
                     f_label = 'Folder:'
@@ -171,16 +172,11 @@
         self.error_save_to_button.setText(translate('SongsPlugin.ImportWizardForm', 'Save to File'))
         # Align all QFormLayouts towards each other.
         formats = [f for f in SongFormat.get_format_list() if 'filepathLabel' in self.format_widgets[f]]
-        labels = [self.format_widgets[f]['filepathLabel'] for f in formats]
+        labels = [self.format_widgets[f]['filepathLabel'] for f in formats] + [self.format_label]
         # Get max width of all labels
-        max_label_width = max(self.format_label.minimumSizeHint().width(),
-                              max([label.minimumSizeHint().width() for label in labels]))
-        self.format_spacer.changeSize(max_label_width, 0, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
-        spacers = [self.format_widgets[f]['filepathSpacer'] for f in formats]
-        for index, spacer in enumerate(spacers):
-            spacer.changeSize(
-                max_label_width - labels[index].minimumSizeHint().width(), 0,
-                QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
+        max_label_width = max(labels, key=lambda label: label.minimumSizeHint().width()).minimumSizeHint().width()
+        for label in labels:
+            label.setFixedWidth(max_label_width)
         # Align descriptionLabels with rest of layout
         for format_list in SongFormat.get_format_list():
             if SongFormat.get(format_list, 'descriptionText') is not None:
@@ -208,13 +204,13 @@
             Settings().setValue('songs/last import type', this_format)
             select_mode, class_, error_msg = SongFormat.get(this_format, 'selectMode', 'class', 'invalidSourceMsg')
             if select_mode == SongFormatSelect.MultipleFiles:
-                import_source = self.get_list_of_files(self.format_widgets[this_format]['file_list_widget'])
+                import_source = self.get_list_of_paths(self.format_widgets[this_format]['file_list_widget'])
                 error_title = UiStrings().IFSp
                 focus_button = self.format_widgets[this_format]['addButton']
             else:
-                import_source = self.format_widgets[this_format]['file_path_edit'].text()
+                import_source = self.format_widgets[this_format]['path_edit'].path
                 error_title = (UiStrings().IFSs if select_mode == SongFormatSelect.SingleFile else UiStrings().IFdSs)
-                focus_button = self.format_widgets[this_format]['browseButton']
+                focus_button = self.format_widgets[this_format]['path_edit']
             if not class_.is_valid_source(import_source):
                 critical_error_message_box(error_title, error_msg)
                 focus_button.setFocus()
@@ -237,21 +233,22 @@
         if filters:
             filters += ';;'
         filters += '{text} (*)'.format(text=UiStrings().AllFiles)
-        file_names = FileDialog.getOpenFileNames(
+        file_paths = PQFileDialog.getOpenFileNames(
             self, title,
-            Settings().value(self.plugin.settings_section + '/last directory import'), filters)
-        if file_names:
-            listbox.addItems(file_names)
-            Settings().setValue(self.plugin.settings_section + '/last directory import',
-                                os.path.split(str(file_names[0]))[0])
+            Settings().path_value(self.plugin.settings_section + '/last directory import'), filters)
+        for file_path in file_paths:
+            list_item = QtWidgets.QListWidgetItem(str(file_path))
+            list_item.setData(QtCore.Qt.UserRole, file_path)
+            listbox.addItem(file_path)
+        Settings().set_path_value(self.plugin.settings_section + '/last directory import', file_paths[0].parent)
 
-    def get_list_of_files(self, list_box):
+    def get_list_of_paths(self, list_box):
         """
         Return a list of file from the list_box
 
         :param list_box: The source list box
         """
-        return [list_box.item(i).text() for i in range(list_box.count())]
+        return [list_box.item(i).data(QtCore.Qt.UserRole) for i in range(list_box.count())]
 
     def remove_selected_items(self, list_box):
         """
@@ -263,20 +260,6 @@
             item = list_box.takeItem(list_box.row(item))
             del item
 
-    def on_browse_button_clicked(self):
-        """
-        Browse for files or a directory.
-        """
-        this_format = self.current_format
-        select_mode, format_name, ext_filter = SongFormat.get(this_format, 'selectMode', 'name', 'filter')
-        file_path_edit = self.format_widgets[this_format]['file_path_edit']
-        if select_mode == SongFormatSelect.SingleFile:
-            self.get_file_name(WizardStrings.OpenTypeFile.format(file_type=format_name),
-                               file_path_edit, 'last directory import', ext_filter)
-        elif select_mode == SongFormatSelect.SingleFolder:
-            self.get_folder(
-                WizardStrings.OpenTypeFolder.format(folder_name=format_name), file_path_edit, 'last directory import')
-
     def on_add_button_clicked(self):
         """
         Add a file or directory.
@@ -296,10 +279,11 @@
         self.remove_selected_items(self.format_widgets[self.current_format]['file_list_widget'])
         self.source_page.completeChanged.emit()
 
-    def on_filepath_edit_text_changed(self):
+    def on_path_edit_path_changed(self):
         """
         Called when the content of the Filename/Folder edit box changes.
         """
+        # TODO: This works out some where!
         self.source_page.completeChanged.emit()
 
     def set_defaults(self):
@@ -317,8 +301,6 @@
             select_mode = SongFormat.get(format_list, 'selectMode')
             if select_mode == SongFormatSelect.MultipleFiles:
                 self.format_widgets[format_list]['file_list_widget'].clear()
-            else:
-                self.format_widgets[format_list]['file_path_edit'].setText('')
         self.error_report_text_edit.clear()
         self.error_report_text_edit.setHidden(True)
         self.error_copy_to_button.setHidden(True)
@@ -341,14 +323,14 @@
         select_mode = SongFormat.get(source_format, 'selectMode')
         if select_mode == SongFormatSelect.SingleFile:
             importer = self.plugin.import_songs(source_format,
-                                                filename=self.format_widgets[source_format]['file_path_edit'].text())
+                                                file_path=self.format_widgets[source_format]['path_edit'].path)
         elif select_mode == SongFormatSelect.SingleFolder:
             importer = self.plugin.import_songs(source_format,
-                                                folder=self.format_widgets[source_format]['file_path_edit'].text())
+                                                folder_path=self.format_widgets[source_format]['path_edit'].path)
         else:
             importer = self.plugin.import_songs(
                 source_format,
-                filenames=self.get_list_of_files(self.format_widgets[source_format]['file_list_widget']))
+                file_paths=self.get_list_of_paths(self.format_widgets[source_format]['file_list_widget']))
         importer.do_import()
         self.progress_label.setText(WizardStrings.FinishedImport)
 
@@ -362,21 +344,19 @@
         """
         Save the error report to a file.
         """
-        filename, filter_used = QtWidgets.QFileDialog.getSaveFileName(
-            self, Settings().value(self.plugin.settings_section + '/last directory import'))
-        if not filename:
+        file_path, filter_used = PQFileDialog.getSaveFileName(
+            self, Settings().path_value(self.plugin.settings_section + '/last directory import'))
+        if file_path is None:
             return
-        report_file = codecs.open(filename, 'w', 'utf-8')
-        report_file.write(self.error_report_text_edit.toPlainText())
-        report_file.close()
+        file_path.write_text(self.error_report_text_edit.toPlainText())
 
     def add_file_select_item(self):
         """
         Add a file selection page.
         """
         this_format = self.current_format
-        prefix, can_disable, description_text, select_mode = \
-            SongFormat.get(this_format, 'prefix', 'canDisable', 'descriptionText', 'selectMode')
+        format_name, prefix, can_disable, description_text, select_mode, filters = \
+            SongFormat.get(this_format, 'name', 'prefix', 'canDisable', 'descriptionText', 'selectMode', 'filter')
         page = QtWidgets.QWidget()
         page.setObjectName(prefix + 'Page')
         if can_disable:
@@ -402,26 +382,23 @@
         if select_mode == SongFormatSelect.SingleFile or select_mode == SongFormatSelect.SingleFolder:
             file_path_layout = QtWidgets.QHBoxLayout()
             file_path_layout.setObjectName(prefix + '_file_path_layout')
-            file_path_layout.setContentsMargins(0, self.format_v_spacing, 0, 0)
             file_path_label = QtWidgets.QLabel(import_widget)
-            file_path_label.setObjectName(prefix + 'FilepathLabel')
             file_path_layout.addWidget(file_path_label)
-            file_path_spacer = QtWidgets.QSpacerItem(0, 0, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
-            file_path_layout.addSpacerItem(file_path_spacer)
-            file_path_edit = QtWidgets.QLineEdit(import_widget)
-            file_path_edit.setObjectName(prefix + '_file_path_edit')
-            file_path_layout.addWidget(file_path_edit)
-            browse_button = QtWidgets.QToolButton(import_widget)
-            browse_button.setIcon(self.open_icon)
-            browse_button.setObjectName(prefix + 'BrowseButton')
-            file_path_layout.addWidget(browse_button)
+            if select_mode == SongFormatSelect.SingleFile:
+                path_type = PathType.Files
+                dialog_caption = WizardStrings.OpenTypeFile.format(file_type=format_name)
+            else:
+                path_type = PathType.Directories
+                dialog_caption = WizardStrings.OpenTypeFolder.format(folder_name=format_name)
+            path_edit = PathEdit(
+                parent=import_widget, path_type=path_type, dialog_caption=dialog_caption, show_revert=False)
+            path_edit.filters = path_edit.filters + filters
+            path_edit.path = Settings().path_value(self.plugin.settings_section + '/last directory import')
+            file_path_layout.addWidget(path_edit)
             import_layout.addLayout(file_path_layout)
             import_layout.addSpacerItem(self.stack_spacer)
             self.format_widgets[this_format]['filepathLabel'] = file_path_label
-            self.format_widgets[this_format]['filepathSpacer'] = file_path_spacer
-            self.format_widgets[this_format]['file_path_layout'] = file_path_layout
-            self.format_widgets[this_format]['file_path_edit'] = file_path_edit
-            self.format_widgets[this_format]['browseButton'] = browse_button
+            self.format_widgets[this_format]['path_edit'] = path_edit
         elif select_mode == SongFormatSelect.MultipleFiles:
             file_list_widget = QtWidgets.QListWidget(import_widget)
             file_list_widget.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
@@ -504,7 +481,8 @@
                 if wizard.format_widgets[this_format]['file_list_widget'].count() > 0:
                     return True
             else:
-                file_path = str(wizard.format_widgets[this_format]['file_path_edit'].text())
+                # TODO: To Path object
+                file_path = str(wizard.format_widgets[this_format]['path_edit'].path)
                 if file_path:
                     if select_mode == SongFormatSelect.SingleFile and os.path.isfile(file_path):
                         return True

=== modified file 'openlp/plugins/songs/lib/__init__.py'
--- openlp/plugins/songs/lib/__init__.py	2017-05-22 19:07:07 +0000
+++ openlp/plugins/songs/lib/__init__.py	2017-06-19 07:33:18 +0000
@@ -538,9 +538,9 @@
         except OSError:
             log.exception('Could not remove file: {name}'.format(name=media_file.file_name))
     try:
-        save_path = os.path.join(AppLocation.get_section_data_path(song_plugin.name), 'audio', str(song_id))
-        if os.path.exists(save_path):
-            os.rmdir(save_path)
+        save_path = AppLocation.get_section_data_path(song_plugin.name) / 'audio' / str(song_id)
+        if save_path.exists():
+            save_path.rmdir()
     except OSError:
         log.exception('Could not remove directory: {path}'.format(path=save_path))
     song_plugin.manager.delete_object(Song, song_id)

=== modified file 'openlp/plugins/songs/lib/importers/cclifile.py'
--- openlp/plugins/songs/lib/importers/cclifile.py	2016-12-31 11:01:36 +0000
+++ openlp/plugins/songs/lib/importers/cclifile.py	2017-06-19 07:33:18 +0000
@@ -20,10 +20,10 @@
 # Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
 ###############################################################################
 
-import logging
-import os
 import chardet
 import codecs
+import logging
+from pathlib2 import Path
 
 from openlp.core.lib import translate
 from openlp.plugins.songs.lib import VerseType
@@ -48,7 +48,7 @@
         :param manager: The song manager for the running OpenLP installation.
         :param kwargs:  The files to be imported.
         """
-        super(CCLIFileImport, self).__init__(manager, **kwargs)
+        super().__init__(manager, **kwargs)
 
     def do_import(self):
         """
@@ -56,37 +56,35 @@
         """
         log.debug('Starting CCLI File Import')
         self.import_wizard.progress_bar.setMaximum(len(self.import_source))
-        for filename in self.import_source:
-            filename = str(filename)
-            log.debug('Importing CCLI File: {name}'.format(name=filename))
-            if os.path.isfile(filename):
-                detect_file = open(filename, 'rb')
-                detect_content = detect_file.read(2048)
-                try:
-                    str(detect_content, 'utf-8')
-                    details = {'confidence': 1, 'encoding': 'utf-8'}
-                except UnicodeDecodeError:
-                    details = chardet.detect(detect_content)
-                detect_file.close()
-                infile = codecs.open(filename, 'r', details['encoding'])
-                if not infile.read(1) == '\ufeff':
+        for file_path in self.import_source:
+            log.debug('Importing CCLI File: {name}'.format(name=file_path))
+            if file_path.is_file():
+                with file_path.open('rb') as detect_file:
+                    detect_content = detect_file.read(2048)
+                    try:
+                        str(detect_content, 'utf-8')
+                        details = {'confidence': 1, 'encoding': 'utf-8'}
+                    except UnicodeDecodeError:
+                        details = chardet.detect(detect_content)
+                in_file = codecs.open(str(file_path), 'r', details['encoding'])
+                if not in_file.read(1) == '\ufeff':
                     # not UTF or no BOM was found
-                    infile.seek(0)
-                lines = infile.readlines()
-                infile.close()
-                ext = os.path.splitext(filename)[1]
-                if ext.lower() == '.usr' or ext.lower() == '.bin':
-                    log.info('SongSelect USR format file found: {name}'.format(name=filename))
+                    in_file.seek(0)
+                lines = in_file.readlines()
+                in_file.close()
+                ext = file_path.suffix.lower()
+                if ext == '.usr' or ext == '.bin':
+                    log.info('SongSelect USR format file found: {name}'.format(name=file_path))
                     if not self.do_import_usr_file(lines):
-                        self.log_error(filename)
-                elif ext.lower() == '.txt':
-                    log.info('SongSelect TEXT format file found: {name}'.format(name=filename))
+                        self.log_error(file_path)
+                elif ext == '.txt':
+                    log.info('SongSelect TEXT format file found: {name}'.format(name=file_path))
                     if not self.do_import_txt_file(lines):
-                        self.log_error(filename)
+                        self.log_error(file_path)
                 else:
-                    self.log_error(filename, translate('SongsPlugin.CCLIFileImport', 'The file does not have a valid '
+                    self.log_error(file_path, translate('SongsPlugin.CCLIFileImport', 'The file does not have a valid '
                                                                                      'extension.'))
-                    log.info('Extension {name} is not valid'.format(name=filename))
+                    log.info('Extension {name} is not valid'.format(name=file_path))
             if self.stop_import_flag:
                 return
 

=== modified file 'openlp/plugins/songs/lib/importers/chordpro.py'
--- openlp/plugins/songs/lib/importers/chordpro.py	2017-02-26 21:14:49 +0000
+++ openlp/plugins/songs/lib/importers/chordpro.py	2017-06-19 07:33:18 +0000
@@ -26,6 +26,7 @@
 
 import logging
 import re
+from pathlib2 import Path
 
 from openlp.core.common import Settings
 
@@ -47,12 +48,11 @@
     """
     def do_import(self):
         self.import_wizard.progress_bar.setMaximum(len(self.import_source))
-        for filename in self.import_source:
+        for file_path in self.import_source:
             if self.stop_import_flag:
                 return
-            song_file = open(filename, 'rt')
-            self.do_import_file(song_file)
-            song_file.close()
+            with file_path.open('rt') as song_file:
+                self.do_import_file(song_file)
 
     def do_import_file(self, song_file):
         """

=== modified file 'openlp/plugins/songs/lib/importers/dreambeam.py'
--- openlp/plugins/songs/lib/importers/dreambeam.py	2016-12-31 11:01:36 +0000
+++ openlp/plugins/songs/lib/importers/dreambeam.py	2017-06-19 07:33:18 +0000
@@ -23,6 +23,7 @@
 The :mod:`dreambeam` module provides the functionality for importing DreamBeam songs into the OpenLP database.
 """
 import logging
+from pathlib2 import Path
 
 from lxml import etree, objectify
 
@@ -78,27 +79,28 @@
 
     def do_import(self):
         """
-        Receive a single file or a list of files to import.
+        Receive a single file_path or a list of files to import.
         """
         if isinstance(self.import_source, list):
             self.import_wizard.progress_bar.setMaximum(len(self.import_source))
-            for file in self.import_source:
+            for file_path in self.import_source:
                 if self.stop_import_flag:
                     return
                 self.set_defaults()
                 parser = etree.XMLParser(remove_blank_text=True)
                 try:
-                    parsed_file = etree.parse(open(file, 'r'), parser)
+                    with file_path.open('r') as xml_file:
+                        parsed_file = etree.parse(xml_file, parser)
                 except etree.XMLSyntaxError:
-                    log.exception('XML syntax error in file {name}'.format(name=file))
-                    self.log_error(file, SongStrings.XMLSyntaxError)
+                    log.exception('XML syntax error in file_path {name}'.format(name=file_path))
+                    self.log_error(file_path, SongStrings.XMLSyntaxError)
                     continue
                 xml = etree.tostring(parsed_file).decode()
                 song_xml = objectify.fromstring(xml)
                 if song_xml.tag != 'DreamSong':
                     self.log_error(
-                        file,
-                        translate('SongsPlugin.DreamBeamImport', 'Invalid DreamBeam song file. Missing DreamSong tag.'))
+                        file_path,
+                        translate('SongsPlugin.DreamBeamImport', 'Invalid DreamBeam song file_path. Missing DreamSong tag.'))
                     continue
                 if hasattr(song_xml, 'Version'):
                     self.version = float(song_xml.Version.text)
@@ -144,4 +146,4 @@
                     else:
                         self.parse_author(author_copyright)
                 if not self.finish():
-                    self.log_error(file)
+                    self.log_error(file_path)

=== modified file 'openlp/plugins/songs/lib/importers/easyslides.py'
--- openlp/plugins/songs/lib/importers/easyslides.py	2017-01-12 21:31:01 +0000
+++ openlp/plugins/songs/lib/importers/easyslides.py	2017-06-19 07:33:18 +0000
@@ -22,6 +22,7 @@
 
 import logging
 import re
+from pathlib2 import Path
 
 from lxml import etree, objectify
 
@@ -47,7 +48,7 @@
     def do_import(self):
         log.info('Importing EasySlides XML file {source}'.format(source=self.import_source))
         parser = etree.XMLParser(remove_blank_text=True, recover=True)
-        parsed_file = etree.parse(self.import_source, parser)
+        parsed_file = etree.parse(str(self.import_source), parser)
         xml = etree.tostring(parsed_file).decode()
         song_xml = objectify.fromstring(xml)
         self.import_wizard.progress_bar.setMaximum(len(song_xml.Item))

=== modified file 'openlp/plugins/songs/lib/importers/easyworship.py'
--- openlp/plugins/songs/lib/importers/easyworship.py	2017-04-01 04:45:12 +0000
+++ openlp/plugins/songs/lib/importers/easyworship.py	2017-06-19 07:33:18 +0000
@@ -23,11 +23,13 @@
 The :mod:`easyworship` module provides the functionality for importing EasyWorship song databases into OpenLP.
 """
 
+import logging
 import os
+import re
 import struct
-import re
 import zlib
-import logging
+from pathlib2 import Path
+
 import sqlite3
 
 from openlp.core.lib import translate
@@ -76,9 +78,11 @@
         """
         Determines the type of file to import and calls the appropiate method
         """
-        if self.import_source.lower().endswith('ews'):
+        self.import_source = Path(self.import_source)
+        ext = self.import_source.suffix.lower()
+        if ext == '.ews':
             self.import_ews()
-        elif self.import_source.endswith('DB'):
+        elif ext == '.db':
             self.import_db()
         else:
             self.import_sqlite_db()
@@ -91,11 +95,11 @@
         or here: http://wiki.openlp.org/Development:EasyWorship_EWS_Format
         """
         # Open ews file if it exists
-        if not os.path.isfile(self.import_source):
+        if not self.import_source.is_file():
             log.debug('Given ews file does not exists.')
             return
         # Make sure there is room for at least a header and one entry
-        if os.path.getsize(self.import_source) < 892:
+        if self.import_source.stat().st_size < 892:
             log.debug('Given ews file is to small to contain valid data.')
             return
         # Take a stab at how text is encoded
@@ -104,7 +108,7 @@
         if not self.encoding:
             log.debug('No encoding set.')
             return
-        self.ews_file = open(self.import_source, 'rb')
+        self.ews_file = self.import_source.open('rb')
         # EWS header, version '1.6'/'  3'/'  5':
         # Offset   Field             Data type    Length    Details
         # --------------------------------------------------------------------------------------------------
@@ -199,23 +203,22 @@
         Import the songs from the database
         """
         # Open the DB and MB files if they exist
-        import_source_mb = self.import_source.replace('.DB', '.MB')
-        if not os.path.isfile(self.import_source):
+        import_source_mb = self.import_source.with_suffix('.MB')
+        if not self.import_source.is_file():
             self.log_error(self.import_source, translate('SongsPlugin.EasyWorshipSongImport',
                                                          'This file does not exist.'))
             return
-        if not os.path.isfile(import_source_mb):
+        if not import_source_mb.is_file():
             self.log_error(self.import_source, translate('SongsPlugin.EasyWorshipSongImport',
                                                          'Could not find the "Songs.MB" file. It must be in the same '
                                                          'folder as the "Songs.DB" file.'))
             return
-        db_size = os.path.getsize(self.import_source)
-        if db_size < 0x800:
+        if self.import_source.stat().st_size < 0x800:
             self.log_error(self.import_source, translate('SongsPlugin.EasyWorshipSongImport',
                                                          'This file is not a valid EasyWorship database.'))
             return
-        db_file = open(self.import_source, 'rb')
-        self.memo_file = open(import_source_mb, 'rb')
+        db_file = self.import_source.open('rb')
+        self.memo_file = import_source_mb.open('rb')
         # Don't accept files that are clearly not paradox files
         record_size, header_size, block_size, first_block, num_fields = struct.unpack('<hhxb8xh17xh', db_file.read(35))
         if header_size != 0x800 or block_size < 1 or block_size > 4:
@@ -340,52 +343,34 @@
         db_file.close()
         self.memo_file.close()
 
-    def _find_file(self, base_path, path_list):
-        """
-        Find the specified file, with the option of the file being at any level in the specified directory structure.
-
-        :param base_path: the location search in
-        :param path_list: the targeted file, preceded by directories that may be their parents relative to the base_path
-        :return: path for targeted file
-        """
-        target_file = ''
-        while len(path_list) > 0:
-            target_file = os.path.join(path_list[-1], target_file)
-            path_list = path_list[:len(path_list) - 1]
-            full_path = os.path.join(base_path, target_file)
-            full_path = full_path[:len(full_path) - 1]
-            if os.path.isfile(full_path):
-                return full_path
-        return ''
-
     def import_sqlite_db(self):
         """
         Import the songs from an EasyWorship 6 SQLite database
         """
-        songs_db_path = self._find_file(self.import_source, ["Databases", "Data", "Songs.db"])
-        song_words_db_path = self._find_file(self.import_source, ["Databases", "Data", "SongWords.db"])
-        invalid_dir_msg = 'This does not appear to be a valid Easy Worship 6 database directory.'
+        songs_db_path = next(self.import_source.rglob('Songs.db'), None)
+        song_words_db_path = next(self.import_source.rglob('SongWords.db'), None)
+        invalid_dir_msg = translate('SongsPlugin.EasyWorshipSongImport',
+                                    'This does not appear to be a valid Easy Worship 6 database directory.')
+        invalid_db_msg = translate('SongsPlugin.EasyWorshipSongImport', 'This is not a valid Easy Worship 6 database.')
         # check to see if needed files are there
-        if not os.path.isfile(songs_db_path):
-            self.log_error(songs_db_path, translate('SongsPlugin.EasyWorshipSongImport', invalid_dir_msg))
+        if not (songs_db_path and songs_db_path.is_file()):
+            self.log_error(self.import_source, invalid_dir_msg)
             return
-        if not os.path.isfile(song_words_db_path):
-            self.log_error(song_words_db_path, translate('SongsPlugin.EasyWorshipSongImport', invalid_dir_msg))
+        if not (song_words_db_path and song_words_db_path.is_file()):
+            self.log_error(self.import_source, invalid_dir_msg)
             return
         # get database handles
-        songs_conn = sqlite3.connect(songs_db_path)
-        words_conn = sqlite3.connect(song_words_db_path)
+        songs_conn = sqlite3.connect(str(songs_db_path))
+        words_conn = sqlite3.connect(str(song_words_db_path))
         if songs_conn is None or words_conn is None:
-            self.log_error(self.import_source, translate('SongsPlugin.EasyWorshipSongImport',
-                                                         'This is not a valid Easy Worship 6 database.'))
+            self.log_error(self.import_source, invalid_db_msg)
             songs_conn.close()
             words_conn.close()
             return
         songs_db = songs_conn.cursor()
         words_db = words_conn.cursor()
         if songs_conn is None or words_conn is None:
-            self.log_error(self.import_source, translate('SongsPlugin.EasyWorshipSongImport',
-                                                         'This is not a valid Easy Worship 6 database.'))
+            self.log_error(self.import_source, invalid_db_msg)
             songs_conn.close()
             words_conn.close()
             return

=== modified file 'openlp/plugins/songs/lib/importers/foilpresenter.py'
--- openlp/plugins/songs/lib/importers/foilpresenter.py	2017-05-30 18:42:35 +0000
+++ openlp/plugins/songs/lib/importers/foilpresenter.py	2017-06-19 07:33:18 +0000
@@ -86,6 +86,7 @@
 import logging
 import re
 import os
+from pathlib2 import Path
 
 from lxml import etree, objectify
 
@@ -121,10 +122,9 @@
         for file_path in self.import_source:
             if self.stop_import_flag:
                 return
-            self.import_wizard.increment_progress_bar(
-                WizardStrings.ImportingType.format(source=os.path.basename(file_path)))
+            self.import_wizard.increment_progress_bar(WizardStrings.ImportingType.format(source=file_path.name))
             try:
-                parsed_file = etree.parse(file_path, parser)
+                parsed_file = etree.parse(str(file_path), parser)
                 xml = etree.tostring(parsed_file).decode()
                 self.foil_presenter.xml_to_song(xml)
             except etree.XMLSyntaxError:

=== modified file 'openlp/plugins/songs/lib/importers/lyrix.py'
--- openlp/plugins/songs/lib/importers/lyrix.py	2016-12-31 11:01:36 +0000
+++ openlp/plugins/songs/lib/importers/lyrix.py	2017-06-19 07:33:18 +0000
@@ -25,6 +25,7 @@
 
 import logging
 import re
+from pathlib2 import Path
 
 from openlp.core.common import translate
 from openlp.plugins.songs.lib.importers.songimport import SongImport
@@ -50,12 +51,11 @@
         if not isinstance(self.import_source, list):
             return
         self.import_wizard.progress_bar.setMaximum(len(self.import_source))
-        for filename in self.import_source:
+        for file_path in self.import_source:
             if self.stop_import_flag:
                 return
-            song_file = open(filename, 'rt', encoding='cp1251')
-            self.do_import_file(song_file)
-            song_file.close()
+            with file_path.open('rt', encoding='cp1251') as song_file:
+                self.do_import_file(song_file)
 
     def do_import_file(self, file):
         """

=== modified file 'openlp/plugins/songs/lib/importers/openlyrics.py'
--- openlp/plugins/songs/lib/importers/openlyrics.py	2017-05-30 18:42:35 +0000
+++ openlp/plugins/songs/lib/importers/openlyrics.py	2017-06-19 07:33:18 +0000
@@ -25,7 +25,7 @@
 """
 
 import logging
-import os
+from pathlib2 import Path
 
 from lxml import etree
 
@@ -58,12 +58,11 @@
         for file_path in self.import_source:
             if self.stop_import_flag:
                 return
-            self.import_wizard.increment_progress_bar(
-                WizardStrings.ImportingType.format(source=os.path.basename(file_path)))
+            self.import_wizard.increment_progress_bar(WizardStrings.ImportingType.format(source=file_path.name))
             try:
                 # Pass a file object, because lxml does not cope with some
                 # special characters in the path (see lp:757673 and lp:744337).
-                parsed_file = etree.parse(open(file_path, 'rb'), parser)
+                parsed_file = etree.parse(file_path.open('rb'), parser)
                 xml = etree.tostring(parsed_file).decode()
                 self.open_lyrics.xml_to_song(xml)
             except etree.XMLSyntaxError:

=== modified file 'openlp/plugins/songs/lib/importers/openoffice.py'
--- openlp/plugins/songs/lib/importers/openoffice.py	2016-12-31 11:01:36 +0000
+++ openlp/plugins/songs/lib/importers/openoffice.py	2017-06-19 07:33:18 +0000
@@ -22,6 +22,7 @@
 import logging
 import os
 import time
+from pathlib2 import Path
 
 from PyQt5 import QtCore
 
@@ -70,12 +71,11 @@
             log.error(exc)
             return
         self.import_wizard.progress_bar.setMaximum(len(self.import_source))
-        for filename in self.import_source:
+        for file_path in self.import_source:
             if self.stop_import_flag:
                 break
-            filename = str(filename)
-            if os.path.isfile(filename):
-                self.open_ooo_file(filename)
+            if file_path.is_file():
+                self.open_ooo_file(file_path)
                 if self.document:
                     self.process_ooo_document()
                     self.close_ooo_file()
@@ -145,11 +145,10 @@
         """
         self.file_path = file_path
         if is_win():
-            url = file_path.replace('\\', '/')
-            url = url.replace(':', '|').replace(' ', '%20')
-            url = 'file:///' + url
+            # TODOL Checked I havn't borked this (by removing replace(':', '|')
+            url = file_path.as_uri()
         else:
-            url = uno.systemPathToFileUrl(file_path)
+            url = uno.systemPathToFileUrl(str(file_path))
         properties = []
         properties.append(self.create_property('Hidden', True))
         properties = tuple(properties)
@@ -159,7 +158,7 @@
                     self.document.supportsService("com.sun.star.text.TextDocument"):
                 self.close_ooo_file()
             else:
-                self.import_wizard.increment_progress_bar('Processing file ' + file_path, 0)
+                self.import_wizard.increment_progress_bar('Processing file {file_path}'.format(file_path), 0)
         except AttributeError:
             log.exception("open_ooo_file failed: {url}".format(url=url))
         return

=== modified file 'openlp/plugins/songs/lib/importers/opensong.py'
--- openlp/plugins/songs/lib/importers/opensong.py	2017-02-26 21:14:49 +0000
+++ openlp/plugins/songs/lib/importers/opensong.py	2017-06-19 07:33:18 +0000
@@ -22,6 +22,7 @@
 
 import logging
 import re
+from pathlib2 import Path
 
 from lxml import objectify
 from lxml.etree import Error, LxmlError
@@ -116,12 +117,11 @@
         if not isinstance(self.import_source, list):
             return
         self.import_wizard.progress_bar.setMaximum(len(self.import_source))
-        for filename in self.import_source:
+        for file_path in self.import_source:
             if self.stop_import_flag:
                 return
-            song_file = open(filename, 'rb')
-            self.do_import_file(song_file)
-            song_file.close()
+            with file_path.open('rb') as song_file:
+                self.do_import_file(song_file)
 
     def do_import_file(self, file):
         """

=== modified file 'openlp/plugins/songs/lib/importers/opspro.py'
--- openlp/plugins/songs/lib/importers/opspro.py	2016-12-31 11:01:36 +0000
+++ openlp/plugins/songs/lib/importers/opspro.py	2017-06-19 07:33:18 +0000
@@ -56,7 +56,7 @@
         password = self.extract_mdb_password()
         try:
             conn = pyodbc.connect('DRIVER={{Microsoft Access Driver (*.mdb)}};DBQ={source};'
-                                  'PWD={password}'.format(source=self.import_source, password=password))
+                                  'PWD={password}'.format(source=str(self.import_source), password=password))
         except (pyodbc.DatabaseError, pyodbc.IntegrityError, pyodbc.InternalError, pyodbc.OperationalError) as e:
             log.warning('Unable to connect the OPS Pro database {source}. {error}'.format(source=self.import_source,
                                                                                           error=str(e)))
@@ -231,16 +231,15 @@
         xor_pattern_2k = (0xa1, 0xec, 0x7a, 0x9c, 0xe1, 0x28, 0x34, 0x8a, 0x73, 0x7b, 0xd2, 0xdf, 0x50)
         # Access97 XOR of the source
         xor_pattern_97 = (0x86, 0xfb, 0xec, 0x37, 0x5d, 0x44, 0x9c, 0xfa, 0xc6, 0x5e, 0x28, 0xe6, 0x13)
-        mdb = open(self.import_source, 'rb')
-        mdb.seek(0x14)
-        version = struct.unpack('B', mdb.read(1))[0]
-        # Get encrypted logo
-        mdb.seek(0x62)
-        EncrypFlag = struct.unpack('B', mdb.read(1))[0]
-        # Get encrypted password
-        mdb.seek(0x42)
-        encrypted_password = mdb.read(26)
-        mdb.close()
+        with self.import_source.open('rb') as mdb_file:
+            mdb_file.seek(0x14)
+            version = struct.unpack('B', mdb_file.read(1))[0]
+            # Get encrypted logo
+            mdb_file.seek(0x62)
+            EncrypFlag = struct.unpack('B', mdb_file.read(1))[0]
+            # Get encrypted password
+            mdb_file.seek(0x42)
+            encrypted_password = mdb_file.read(26)
         # "Decrypt" the password based on the version
         decrypted_password = ''
         if version < 0x01:

=== modified file 'openlp/plugins/songs/lib/importers/powerpraise.py'
--- openlp/plugins/songs/lib/importers/powerpraise.py	2017-05-30 18:42:35 +0000
+++ openlp/plugins/songs/lib/importers/powerpraise.py	2017-06-19 07:33:18 +0000
@@ -24,8 +24,8 @@
 Powerpraise song files into the current database.
 """
 
-import os
 from lxml import objectify
+from pathlib2 import Path
 
 from openlp.core.ui.lib.wizard import WizardStrings
 from .songimport import SongImport
@@ -41,10 +41,11 @@
         for file_path in self.import_source:
             if self.stop_import_flag:
                 return
-            self.import_wizard.increment_progress_bar(
-                WizardStrings.ImportingType.format(source=os.path.basename(file_path)))
-            root = objectify.parse(open(file_path, 'rb')).getroot()
-            self.process_song(root)
+            # TODO: Verify format() with template strings
+            self.import_wizard.increment_progress_bar(WizardStrings.ImportingType.format(source=file_path.name))
+            with file_path.open('rb') as xml_file:
+                root = objectify.parse(xml_file).getroot()
+                self.process_song(root)
 
     def process_song(self, root):
         self.set_defaults()

=== modified file 'openlp/plugins/songs/lib/importers/powersong.py'
--- openlp/plugins/songs/lib/importers/powersong.py	2016-12-31 11:01:36 +0000
+++ openlp/plugins/songs/lib/importers/powersong.py	2017-06-19 07:33:18 +0000
@@ -72,10 +72,16 @@
         Checks if source is a PowerSong 1.0 folder:
             * is a directory
             * contains at least one \*.song file
+
+        :param import_source: Should be a Path object that fulfills the above criteria
+        :type import_source: Path
+
+        :return: If the source is valid
+        :rtype: bool
         """
-        if os.path.isdir(import_source):
-            for file in os.listdir(import_source):
-                if fnmatch.fnmatch(file, '*.song'):
+        if import_source.is_dir():
+            for file_path in import_source.iterdir():
+                if file_path.suffix == '.song':
                     return True
         return False
 

=== modified file 'openlp/plugins/songs/lib/importers/presentationmanager.py'
--- openlp/plugins/songs/lib/importers/presentationmanager.py	2017-05-30 18:42:35 +0000
+++ openlp/plugins/songs/lib/importers/presentationmanager.py	2017-06-19 07:33:18 +0000
@@ -23,13 +23,12 @@
 The :mod:`presentationmanager` module provides the functionality for importing
 Presentationmanager song files into the current database.
 """
-import os
 import re
+from pathlib2 import Path
 
-import chardet
 from lxml import objectify, etree
 
-from openlp.core.common import translate
+from openlp.core.common import get_file_encoding, translate
 from openlp.core.ui.lib.wizard import WizardStrings
 from .songimport import SongImport
 
@@ -44,17 +43,14 @@
         for file_path in self.import_source:
             if self.stop_import_flag:
                 return
-            self.import_wizard.increment_progress_bar(
-                WizardStrings.ImportingType.format(source=os.path.basename(file_path)))
+            self.import_wizard.increment_progress_bar(WizardStrings.ImportingType.format(source=file_path.name))
             try:
-                tree = etree.parse(file_path, parser=etree.XMLParser(recover=True))
+                tree = etree.parse(str(file_path), parser=etree.XMLParser(recover=True))
             except etree.XMLSyntaxError:
                 # Try to detect encoding and use it
-                file = open(file_path, mode='rb')
-                encoding = chardet.detect(file.read())['encoding']
-                file.close()
+                encoding = get_file_encoding(file_path)['encoding']
                 # Open file with detected encoding and remove encoding declaration
-                text = open(file_path, mode='r', encoding=encoding).read()
+                text = file_path.read_text(encoding=encoding)
                 text = re.sub('.+\?>\n', '', text)
                 try:
                     tree = etree.fromstring(text, parser=etree.XMLParser(recover=True))
@@ -80,6 +76,14 @@
             return ''
 
     def process_song(self, root, file_path):
+        """
+        :param root: 
+        :param file_path: Path to the file to process
+        :type file_path: Path
+
+        :return: None
+        :rtype: None
+        """
         self.set_defaults()
         attrs = None
         if hasattr(root, 'attributes'):
@@ -123,4 +127,4 @@
 
         self.verse_order_list = verse_order_list
         if not self.finish():
-            self.log_error(os.path.basename(file_path))
+            self.log_error(file_path.name)

=== modified file 'openlp/plugins/songs/lib/importers/propresenter.py'
--- openlp/plugins/songs/lib/importers/propresenter.py	2017-05-30 18:42:35 +0000
+++ openlp/plugins/songs/lib/importers/propresenter.py	2017-06-19 07:33:18 +0000
@@ -28,6 +28,7 @@
 import base64
 import logging
 from lxml import objectify
+from pathlib2 import Path
 
 from openlp.core.ui.lib.wizard import WizardStrings
 from openlp.plugins.songs.lib import strip_rtf
@@ -47,11 +48,20 @@
             if self.stop_import_flag:
                 return
             self.import_wizard.increment_progress_bar(
-                WizardStrings.ImportingType.format(source=os.path.basename(file_path)))
-            root = objectify.parse(open(file_path, 'rb')).getroot()
-            self.process_song(root, file_path)
-
-    def process_song(self, root, filename):
+                WizardStrings.ImportingType.format(source=file_path.name))
+            with file_path.open('rb') as xml_file:
+                root = objectify.parse(xml_file).getroot()
+                self.process_song(root, file_path)
+
+    def process_song(self, root, file_path):
+        """
+        :param root: 
+        :param file_path: Path to the file thats being imported
+        :type file_path: Path
+
+        :return: None
+        :rtype: None
+        """
         self.set_defaults()
 
         # Extract ProPresenter versionNumber
@@ -64,9 +74,7 @@
         # Title
         self.title = root.get('CCLISongTitle')
         if not self.title or self.title == '':
-            self.title = os.path.basename(filename)
-            if self.title[-5:-1] == '.pro':
-                self.title = self.title[:-5]
+            self.title = file_path.stem
         # Notes
         self.comments = root.get('notes')
         # Author

=== modified file 'openlp/plugins/songs/lib/importers/songbeamer.py'
--- openlp/plugins/songs/lib/importers/songbeamer.py	2017-05-11 19:53:47 +0000
+++ openlp/plugins/songs/lib/importers/songbeamer.py	2017-06-19 07:33:18 +0000
@@ -27,6 +27,7 @@
 import re
 import base64
 import math
+from pathlib2 import Path
 
 from openlp.plugins.songs.lib import VerseType
 from openlp.plugins.songs.lib.importers.songimport import SongImport
@@ -111,7 +112,7 @@
         if not isinstance(self.import_source, list):
             return
         self.import_wizard.progress_bar.setMaximum(len(self.import_source))
-        for import_file in self.import_source:
+        for file_path in self.import_source:
             # TODO: check that it is a valid SongBeamer file
             if self.stop_import_flag:
                 return
@@ -119,20 +120,19 @@
             self.current_verse = ''
             self.current_verse_type = VerseType.tags[VerseType.Verse]
             self.chord_table = None
-            file_name = os.path.split(import_file)[1]
-            if os.path.isfile(import_file):
+            if file_path.is_file():
                 # Detect the encoding
-                self.input_file_encoding = get_file_encoding(import_file)['encoding']
+                self.input_file_encoding = get_file_encoding(file_path)['encoding']
                 # The encoding should only be ANSI (cp1252), UTF-8, Unicode, Big-Endian-Unicode.
                 # So if it doesn't start with 'u' we default to cp1252. See:
                 # https://forum.songbeamer.com/viewtopic.php?p=419&sid=ca4814924e37c11e4438b7272a98b6f2
                 if not self.input_file_encoding.lower().startswith('u'):
                     self.input_file_encoding = 'cp1252'
-                infile = open(import_file, 'rt', encoding=self.input_file_encoding)
-                song_data = infile.readlines()
+                with file_path.open(encoding=self.input_file_encoding) as song_file:
+                    song_data = song_file.readlines()
             else:
                 continue
-            self.title = file_name.split('.sng')[0]
+            self.title = file_path.stem
             read_verses = False
             # The first verse separator doesn't count, but the others does, so line count starts at -1
             line_number = -1
@@ -184,7 +184,7 @@
                                 # inserted by songbeamer, but are manually added headings. So restart the loop, and
                                 # count tags as lines.
                                 self.set_defaults()
-                                self.title = file_name.split('.sng')[0]
+                                self.title = file_path.stem
                                 verse_tags_mode = VerseTagMode.ContainsNoTagsRestart
                                 read_verses = False
                                 # The first verseseparator doesn't count, but the others does, so linecount starts at -1
@@ -206,7 +206,7 @@
                 self.replace_html_tags()
                 self.add_verse(self.current_verse, self.current_verse_type)
             if not self.finish():
-                self.log_error(import_file)
+                self.log_error(file_path)
 
     def insert_chords(self, line_number, line):
         """

=== modified file 'openlp/plugins/songs/lib/importers/songimport.py'
--- openlp/plugins/songs/lib/importers/songimport.py	2017-05-30 18:42:35 +0000
+++ openlp/plugins/songs/lib/importers/songimport.py	2017-06-19 07:33:18 +0000
@@ -24,6 +24,7 @@
 import re
 import shutil
 import os
+from pathlib2 import Path
 
 from PyQt5 import QtCore
 
@@ -61,14 +62,14 @@
         """
         self.manager = manager
         QtCore.QObject.__init__(self)
-        if 'filename' in kwargs:
-            self.import_source = kwargs['filename']
-        elif 'filenames' in kwargs:
-            self.import_source = kwargs['filenames']
-        elif 'folder' in kwargs:
-            self.import_source = kwargs['folder']
+        if 'file_path' in kwargs:
+            self.import_source = kwargs['file_path']
+        elif 'file_paths' in kwargs:
+            self.import_source = kwargs['file_paths']
+        elif 'folder_path' in kwargs:
+            self.import_source = kwargs['folder_path']
         else:
-            raise KeyError('Keyword arguments "filename[s]" or "folder" not supplied.')
+            raise KeyError('Keyword arguments "file_path[s]" or "folder_path" not supplied.')
         log.debug(self.import_source)
         self.import_wizard = None
         self.song = None
@@ -421,9 +422,10 @@
         :param filename: The file to copy.
         """
         if not hasattr(self, 'save_path'):
-            self.save_path = os.path.join(AppLocation.get_section_data_path(self.import_wizard.plugin.name),
-                                          'audio', str(song_id))
-        check_directory_exists(self.save_path)
+            # TODO: convert self.save_path to Path object
+            self.save_path = str(
+                AppLocation.get_section_data_path(self.import_wizard.plugin.name) / 'audio' / str(song_id))
+        check_directory_exists(Path(self.save_path))
         if not filename.startswith(self.save_path):
             old_file, filename = filename, os.path.join(self.save_path, os.path.split(filename)[1])
             shutil.copyfile(old_file, filename)

=== modified file 'openlp/plugins/songs/lib/importers/songpro.py'
--- openlp/plugins/songs/lib/importers/songpro.py	2016-12-31 11:01:36 +0000
+++ openlp/plugins/songs/lib/importers/songpro.py	2017-06-19 07:33:18 +0000
@@ -24,6 +24,7 @@
 songs into the OpenLP database.
 """
 import re
+from pathlib2 import Path
 
 from openlp.plugins.songs.lib import strip_rtf
 from openlp.plugins.songs.lib.importers.songimport import SongImport
@@ -72,7 +73,8 @@
         Receive a single file or a list of files to import.
         """
         self.encoding = None
-        with open(self.import_source, 'rt', errors='ignore') as songs_file:
+        self.import_source = Path(self.import_source)
+        with self.import_source.open('rt', errors='ignore') as songs_file:
             self.import_wizard.progress_bar.setMaximum(0)
             tag = ''
             text = ''

=== modified file 'openlp/plugins/songs/lib/importers/songshowplus.py'
--- openlp/plugins/songs/lib/importers/songshowplus.py	2017-06-01 06:18:47 +0000
+++ openlp/plugins/songs/lib/importers/songshowplus.py	2017-06-19 07:33:18 +0000
@@ -27,6 +27,7 @@
 import logging
 import re
 import struct
+from pathlib2 import Path
 
 from openlp.core.ui.lib.wizard import WizardStrings
 from openlp.plugins.songs.lib import VerseType, retrieve_windows_encoding
@@ -93,97 +94,95 @@
         if not isinstance(self.import_source, list):
             return
         self.import_wizard.progress_bar.setMaximum(len(self.import_source))
-        for file in self.import_source:
+        for file_path in self.import_source:
             if self.stop_import_flag:
                 return
             self.ssp_verse_order_list = []
             self.other_count = 0
             self.other_list = {}
-            file_name = os.path.split(file)[1]
-            self.import_wizard.increment_progress_bar(WizardStrings.ImportingType.format(source=file_name), 0)
-            song_data = open(file, 'rb')
-            while True:
-                block_key, = struct.unpack("I", song_data.read(4))
-                log.debug('block_key: %d' % block_key)
-                # The file ends with 4 NULL's
-                if block_key == 0:
-                    break
-                next_block_starts, = struct.unpack("I", song_data.read(4))
-                next_block_starts += song_data.tell()
-                if block_key in (VERSE, CHORUS, BRIDGE):
-                    null, verse_no, = struct.unpack("BB", song_data.read(2))
-                elif block_key == CUSTOM_VERSE:
-                    null, verse_name_length, = struct.unpack("BB", song_data.read(2))
-                    verse_name = self.decode(song_data.read(verse_name_length))
-                length_descriptor_size, = struct.unpack("B", song_data.read(1))
-                log.debug('length_descriptor_size: %d' % length_descriptor_size)
-                # In the case of song_numbers the number is in the data from the
-                # current position to the next block starts
-                if block_key == SONG_NUMBER:
-                    sn_bytes = song_data.read(length_descriptor_size - 1)
-                    self.song_number = int.from_bytes(sn_bytes, byteorder='little')
-                    continue
-                # Detect if/how long the length descriptor is
-                if length_descriptor_size == 12 or length_descriptor_size == 20:
-                    length_descriptor, = struct.unpack("I", song_data.read(4))
-                elif length_descriptor_size == 2:
-                    length_descriptor = 1
-                elif length_descriptor_size == 9:
-                    length_descriptor = 0
-                else:
-                    length_descriptor, = struct.unpack("B", song_data.read(1))
-                log.debug('length_descriptor: %d' % length_descriptor)
-                data = song_data.read(length_descriptor)
-                log.debug(data)
-                if block_key == TITLE:
-                    self.title = self.decode(data)
-                elif block_key == AUTHOR:
-                    authors = self.decode(data).split(" / ")
-                    for author in authors:
-                        if author.find(",") != -1:
-                            author_parts = author.split(", ")
-                            author = author_parts[1] + " " + author_parts[0]
-                        self.parse_author(author)
-                elif block_key == COPYRIGHT:
-                    self.add_copyright(self.decode(data))
-                elif block_key == CCLI_NO:
-                    # Try to get the CCLI number even if the field contains additional text
-                    match = re.search(r'\d+', self.decode(data))
-                    if match:
-                        self.ccli_number = int(match.group())
-                    else:
-                        log.warning("Can't parse CCLI Number from string: {text}".format(text=self.decode(data)))
-                elif block_key == VERSE:
-                    self.add_verse(self.decode(data), "{tag}{number}".format(tag=VerseType.tags[VerseType.Verse],
-                                                                             number=verse_no))
-                elif block_key == CHORUS:
-                    self.add_verse(self.decode(data), "{tag}{number}".format(tag=VerseType.tags[VerseType.Chorus],
-                                                                             number=verse_no))
-                elif block_key == BRIDGE:
-                    self.add_verse(self.decode(data), "{tag}{number}".format(tag=VerseType.tags[VerseType.Bridge],
-                                                                             number=verse_no))
-                elif block_key == TOPIC:
-                    self.topics.append(self.decode(data))
-                elif block_key == COMMENTS:
-                    self.comments = self.decode(data)
-                elif block_key == VERSE_ORDER:
-                    verse_tag = self.to_openlp_verse_tag(self.decode(data), True)
-                    if verse_tag:
-                        if not isinstance(verse_tag, str):
-                            verse_tag = self.decode(verse_tag)
-                        self.ssp_verse_order_list.append(verse_tag)
-                elif block_key == SONG_BOOK:
-                    self.song_book_name = self.decode(data)
-                elif block_key == CUSTOM_VERSE:
-                    verse_tag = self.to_openlp_verse_tag(verse_name)
-                    self.add_verse(self.decode(data), verse_tag)
-                else:
-                    log.debug("Unrecognised blockKey: {key}, data: {data}".format(key=block_key, data=data))
-                    song_data.seek(next_block_starts)
-            self.verse_order_list = self.ssp_verse_order_list
-            song_data.close()
-            if not self.finish():
-                self.log_error(file)
+            self.import_wizard.increment_progress_bar(WizardStrings.ImportingType.format(source=file_path.name), 0)
+            with file_path.open('rb') as song_file:
+                while True:
+                    block_key, = struct.unpack("I", song_file.read(4))
+                    log.debug('block_key: %d' % block_key)
+                    # The file ends with 4 NULL's
+                    if block_key == 0:
+                        break
+                    next_block_starts, = struct.unpack("I", song_file.read(4))
+                    next_block_starts += song_file.tell()
+                    if block_key in (VERSE, CHORUS, BRIDGE):
+                        null, verse_no, = struct.unpack("BB", song_file.read(2))
+                    elif block_key == CUSTOM_VERSE:
+                        null, verse_name_length, = struct.unpack("BB", song_file.read(2))
+                        verse_name = self.decode(song_file.read(verse_name_length))
+                    length_descriptor_size, = struct.unpack("B", song_file.read(1))
+                    log.debug('length_descriptor_size: %d' % length_descriptor_size)
+                    # In the case of song_numbers the number is in the data from the
+                    # current position to the next block starts
+                    if block_key == SONG_NUMBER:
+                        sn_bytes = song_file.read(length_descriptor_size - 1)
+                        self.song_number = int.from_bytes(sn_bytes, byteorder='little')
+                        continue
+                    # Detect if/how long the length descriptor is
+                    if length_descriptor_size == 12 or length_descriptor_size == 20:
+                        length_descriptor, = struct.unpack("I", song_file.read(4))
+                    elif length_descriptor_size == 2:
+                        length_descriptor = 1
+                    elif length_descriptor_size == 9:
+                        length_descriptor = 0
+                    else:
+                        length_descriptor, = struct.unpack("B", song_file.read(1))
+                    log.debug('length_descriptor: %d' % length_descriptor)
+                    data = song_file.read(length_descriptor)
+                    log.debug(data)
+                    if block_key == TITLE:
+                        self.title = self.decode(data)
+                    elif block_key == AUTHOR:
+                        authors = self.decode(data).split(" / ")
+                        for author in authors:
+                            if author.find(",") != -1:
+                                author_parts = author.split(", ")
+                                author = author_parts[1] + " " + author_parts[0]
+                            self.parse_author(author)
+                    elif block_key == COPYRIGHT:
+                        self.add_copyright(self.decode(data))
+                    elif block_key == CCLI_NO:
+                        # Try to get the CCLI number even if the field contains additional text
+                        match = re.search(r'\d+', self.decode(data))
+                        if match:
+                            self.ccli_number = int(match.group())
+                        else:
+                            log.warning("Can't parse CCLI Number from string: {text}".format(text=self.decode(data)))
+                    elif block_key == VERSE:
+                        self.add_verse(self.decode(data), "{tag}{number}".format(tag=VerseType.tags[VerseType.Verse],
+                                                                                 number=verse_no))
+                    elif block_key == CHORUS:
+                        self.add_verse(self.decode(data), "{tag}{number}".format(tag=VerseType.tags[VerseType.Chorus],
+                                                                                 number=verse_no))
+                    elif block_key == BRIDGE:
+                        self.add_verse(self.decode(data), "{tag}{number}".format(tag=VerseType.tags[VerseType.Bridge],
+                                                                                 number=verse_no))
+                    elif block_key == TOPIC:
+                        self.topics.append(self.decode(data))
+                    elif block_key == COMMENTS:
+                        self.comments = self.decode(data)
+                    elif block_key == VERSE_ORDER:
+                        verse_tag = self.to_openlp_verse_tag(self.decode(data), True)
+                        if verse_tag:
+                            if not isinstance(verse_tag, str):
+                                verse_tag = self.decode(verse_tag)
+                            self.ssp_verse_order_list.append(verse_tag)
+                    elif block_key == SONG_BOOK:
+                        self.song_book_name = self.decode(data)
+                    elif block_key == CUSTOM_VERSE:
+                        verse_tag = self.to_openlp_verse_tag(verse_name)
+                        self.add_verse(self.decode(data), verse_tag)
+                    else:
+                        log.debug("Unrecognised blockKey: {key}, data: {data}".format(key=block_key, data=data))
+                        song_file.seek(next_block_starts)
+                self.verse_order_list = self.ssp_verse_order_list
+                if not self.finish():
+                    self.log_error(file_path)
 
     def to_openlp_verse_tag(self, verse_name, ignore_unique=False):
         """

=== modified file 'openlp/plugins/songs/lib/importers/sundayplus.py'
--- openlp/plugins/songs/lib/importers/sundayplus.py	2016-12-31 11:01:36 +0000
+++ openlp/plugins/songs/lib/importers/sundayplus.py	2017-06-19 07:33:18 +0000
@@ -22,7 +22,7 @@
 
 import os
 import re
-import logging
+from pathlib2 import Path
 
 
 from openlp.plugins.songs.lib import VerseType, retrieve_windows_encoding
@@ -60,12 +60,11 @@
 
     def do_import(self):
         self.import_wizard.progress_bar.setMaximum(len(self.import_source))
-        for filename in self.import_source:
+        for file_path in self.import_source:
             if self.stop_import_flag:
                 return
-            song_file = open(filename, 'rb')
-            self.do_import_file(song_file)
-            song_file.close()
+            with file_path.open('rb') as song_file:
+                self.do_import_file(song_file)
 
     def do_import_file(self, file):
         """

=== modified file 'openlp/plugins/songs/lib/importers/videopsalm.py'
--- openlp/plugins/songs/lib/importers/videopsalm.py	2017-02-26 21:14:49 +0000
+++ openlp/plugins/songs/lib/importers/videopsalm.py	2017-06-19 07:33:18 +0000
@@ -23,10 +23,11 @@
 The :mod:`lyrix` module provides the functionality for importing songs which are
 exproted from Lyrix."""
 
+import json
 import logging
-import json
 import os
 import re
+from pathlib2 import Path
 
 from openlp.core.common import translate, Settings
 from openlp.plugins.songs.lib.importers.songimport import SongImport
@@ -50,11 +51,10 @@
         """
         Process the VideoPsalm file - pass in a file-like object, not a file path.
         """
+        self.import_source = Path(self.import_source)
         self.set_defaults()
-        # Open SongBook file
-        song_file = open(self.import_source, 'rt', encoding='utf-8-sig')
         try:
-            file_content = song_file.read()
+            file_content = self.import_source.read_text(encoding='utf-8-sig')
             processed_content = ''
             inside_quotes = False
             # The VideoPsalm format is not valid json, it uses illegal line breaks and unquoted keys, this must be fixed
@@ -89,7 +89,10 @@
             songs = songbook['Songs']
             self.import_wizard.progress_bar.setMaximum(len(songs))
             songbook_name = songbook['Text']
-            media_folder = os.path.normpath(os.path.join(os.path.dirname(song_file.name), '..', 'Audio'))
+            # TODO: This probably never worked!
+            # Originally: media_folder = os.path.normpath(os.path.join(os.path.dirname(song_file.name), '..', 'Audio'))
+            # song_file.name returned just the file name, so it had no directory to operate on.
+            media_folder = Path('..', 'Audio')
             for song in songs:
                 self.song_book_name = songbook_name
                 if 'Text' in song:
@@ -114,7 +117,7 @@
                 if 'Theme' in song:
                     self.topics = song['Theme'].splitlines()
                 if 'AudioFile' in song:
-                    self.add_media_file(os.path.join(media_folder, song['AudioFile']))
+                    self.add_media_file(str(media_folder / song['AudioFile']))
                 if 'Memo1' in song:
                     self.add_comment(song['Memo1'])
                 if 'Memo2' in song:
@@ -132,4 +135,4 @@
                 if not self.finish():
                     self.log_error('Could not import {title}'.format(title=self.title))
         except Exception as e:
-            self.log_error(song_file.name, translate('SongsPlugin.VideoPsalmImport', 'Error: {error}').format(error=e))
+            self.log_error(self.import_source.name, translate('SongsPlugin.VideoPsalmImport', 'Error: {error}').format(error=e))

=== modified file 'openlp/plugins/songs/lib/importers/wordsofworship.py'
--- openlp/plugins/songs/lib/importers/wordsofworship.py	2016-12-31 11:01:36 +0000
+++ openlp/plugins/songs/lib/importers/wordsofworship.py	2017-06-19 07:33:18 +0000
@@ -25,6 +25,7 @@
 """
 import os
 import logging
+from pathlib2 import Path
 
 from openlp.core.common import translate
 from openlp.plugins.songs.lib.importers.songimport import SongImport
@@ -100,62 +101,60 @@
         """
         if isinstance(self.import_source, list):
             self.import_wizard.progress_bar.setMaximum(len(self.import_source))
-            for source in self.import_source:
+            for file_path in self.import_source:
                 if self.stop_import_flag:
                     return
                 self.set_defaults()
-                song_data = open(source, 'rb')
-                if song_data.read(19).decode() != 'WoW File\nSong Words':
-                    self.log_error(source,
-                                   translate('SongsPlugin.WordsofWorshipSongImport',
-                                             'Invalid Words of Worship song file. Missing "{text}" '
-                                             'header.').format(text='WoW File\\nSong Words'))
-                    continue
-                # Seek to byte which stores number of blocks in the song
-                song_data.seek(56)
-                no_of_blocks = ord(song_data.read(1))
-                song_data.seek(66)
-                if song_data.read(16).decode() != 'CSongDoc::CBlock':
-                    self.log_error(source,
-                                   translate('SongsPlugin.WordsofWorshipSongImport',
-                                             'Invalid Words of Worship song file. Missing "{text}" '
-                                             'string.').format(text='CSongDoc::CBlock'))
-                    continue
-                # Seek to the beginning of the first block
-                song_data.seek(82)
-                for block in range(no_of_blocks):
-                    skip_char_at_end = True
-                    self.lines_to_read = ord(song_data.read(4)[:1])
-                    block_text = ''
-                    while self.lines_to_read:
-                        self.line_text = str(song_data.read(ord(song_data.read(1))), 'cp1252')
-                        if skip_char_at_end:
-                            skip_char = ord(song_data.read(1))
-                            # Check if we really should skip a char. In some wsg files we shouldn't
-                            if skip_char != 0:
-                                song_data.seek(-1, os.SEEK_CUR)
-                                skip_char_at_end = False
-                        if block_text:
-                            block_text += '\n'
-                        block_text += self.line_text
-                        self.lines_to_read -= 1
-                    block_type = BLOCK_TYPES[ord(song_data.read(4)[:1])]
-                    # Blocks are separated by 2 bytes, skip them, but not if
-                    # this is the last block!
-                    if block + 1 < no_of_blocks:
-                        song_data.seek(2, os.SEEK_CUR)
-                    self.add_verse(block_text, block_type)
-                # Now to extract the author
-                author_length = ord(song_data.read(1))
-                if author_length:
-                    self.parse_author(str(song_data.read(author_length), 'cp1252'))
-                # Finally the copyright
-                copyright_length = ord(song_data.read(1))
-                if copyright_length:
-                    self.add_copyright(str(song_data.read(copyright_length), 'cp1252'))
-                file_name = os.path.split(source)[1]
-                # Get the song title
-                self.title = file_name.rpartition('.')[0]
-                song_data.close()
-                if not self.finish():
-                    self.log_error(source)
+                with file_path.open('rb') as song_data:
+                    if song_data.read(19).decode() != 'WoW File\nSong Words':
+                        self.log_error(file_path,
+                                       translate('SongsPlugin.WordsofWorshipSongImport',
+                                                 'Invalid Words of Worship song file. Missing "{text}" '
+                                                 'header.').format(text='WoW File\\nSong Words'))
+                        continue
+                    # Seek to byte which stores number of blocks in the song
+                    song_data.seek(56)
+                    no_of_blocks = ord(song_data.read(1))
+                    song_data.seek(66)
+                    if song_data.read(16).decode() != 'CSongDoc::CBlock':
+                        self.log_error(file_path,
+                                       translate('SongsPlugin.WordsofWorshipSongImport',
+                                                 'Invalid Words of Worship song file. Missing "{text}" '
+                                                 'string.').format(text='CSongDoc::CBlock'))
+                        continue
+                    # Seek to the beginning of the first block
+                    song_data.seek(82)
+                    for block in range(no_of_blocks):
+                        skip_char_at_end = True
+                        self.lines_to_read = ord(song_data.read(4)[:1])
+                        block_text = ''
+                        while self.lines_to_read:
+                            self.line_text = str(song_data.read(ord(song_data.read(1))), 'cp1252')
+                            if skip_char_at_end:
+                                skip_char = ord(song_data.read(1))
+                                # Check if we really should skip a char. In some wsg files we shouldn't
+                                if skip_char != 0:
+                                    song_data.seek(-1, os.SEEK_CUR)
+                                    skip_char_at_end = False
+                            if block_text:
+                                block_text += '\n'
+                            block_text += self.line_text
+                            self.lines_to_read -= 1
+                        block_type = BLOCK_TYPES[ord(song_data.read(4)[:1])]
+                        # Blocks are separated by 2 bytes, skip them, but not if
+                        # this is the last block!
+                        if block + 1 < no_of_blocks:
+                            song_data.seek(2, os.SEEK_CUR)
+                        self.add_verse(block_text, block_type)
+                    # Now to extract the author
+                    author_length = ord(song_data.read(1))
+                    if author_length:
+                        self.parse_author(str(song_data.read(author_length), 'cp1252'))
+                    # Finally the copyright
+                    copyright_length = ord(song_data.read(1))
+                    if copyright_length:
+                        self.add_copyright(str(song_data.read(copyright_length), 'cp1252'))
+                    # Get the song title
+                    self.title = file_path.stem
+                    if not self.finish():
+                        self.log_error(file_path)

=== modified file 'openlp/plugins/songs/lib/importers/worshipassistant.py'
--- openlp/plugins/songs/lib/importers/worshipassistant.py	2016-12-31 11:01:36 +0000
+++ openlp/plugins/songs/lib/importers/worshipassistant.py	2017-06-19 07:33:18 +0000
@@ -28,7 +28,7 @@
 import logging
 import re
 
-from openlp.core.common import translate
+from openlp.core.common import get_file_encoding, translate
 from openlp.plugins.songs.lib import VerseType
 from openlp.plugins.songs.lib.importers.songimport import SongImport
 
@@ -81,19 +81,16 @@
         Receive a CSV file to import.
         """
         # Get encoding
-        detect_file = open(self.import_source, 'rb')
-        detect_content = detect_file.read()
-        details = chardet.detect(detect_content)
-        detect_file.close()
-        songs_file = open(self.import_source, 'r', encoding=details['encoding'])
-        songs_reader = csv.DictReader(songs_file, escapechar='\\')
-        try:
-            records = list(songs_reader)
-        except csv.Error as e:
-            self.log_error(translate('SongsPlugin.WorshipAssistantImport', 'Error reading CSV file.'),
-                           translate('SongsPlugin.WorshipAssistantImport',
-                                     'Line {number:d}: {error}').format(number=songs_reader.line_num, error=e))
-            return
+        encoding = get_file_encoding(self.import_source)['encoding']
+        with self.import_source.open('r', encoding=encoding) as songs_file:
+            songs_reader = csv.DictReader(songs_file, escapechar='\\')
+            try:
+                records = list(songs_reader)
+            except csv.Error as e:
+                self.log_error(translate('SongsPlugin.WorshipAssistantImport', 'Error reading CSV file.'),
+                               translate('SongsPlugin.WorshipAssistantImport',
+                                         'Line {number:d}: {error}').format(number=songs_reader.line_num, error=e))
+                return
         num_records = len(records)
         log.info('{count} records found in CSV file'.format(count=num_records))
         self.import_wizard.progress_bar.setMaximum(num_records)
@@ -185,4 +182,3 @@
                 self.log_error(translate('SongsPlugin.WorshipAssistantImport',
                                          'Record {count:d}').format(count=index) +
                                (': "' + self.title + '"' if self.title else ''))
-            songs_file.close()

=== modified file 'openlp/plugins/songs/lib/importers/zionworx.py'
--- openlp/plugins/songs/lib/importers/zionworx.py	2016-12-31 11:01:36 +0000
+++ openlp/plugins/songs/lib/importers/zionworx.py	2017-06-19 07:33:18 +0000
@@ -76,7 +76,7 @@
         Receive a CSV file (from a ZionWorx database dump) to import.
         """
         # Encoding should always be ISO-8859-1
-        with open(self.import_source, 'rt', encoding='ISO-8859-1') as songs_file:
+        with self.import_source.open('rt', encoding='ISO-8859-1') as songs_file:
             field_names = ['SongNum', 'Title1', 'Title2', 'Lyrics', 'Writer', 'Copyright', 'Keywords',
                            'DefaultStyle']
             songs_reader = csv.DictReader(songs_file, field_names)

=== modified file 'openlp/plugins/songs/lib/mediaitem.py'
--- openlp/plugins/songs/lib/mediaitem.py	2017-06-09 06:06:49 +0000
+++ openlp/plugins/songs/lib/mediaitem.py	2017-06-19 07:33:18 +0000
@@ -23,6 +23,7 @@
 import logging
 import os
 import shutil
+from pathlib2 import Path
 
 from PyQt5 import QtCore, QtWidgets
 from sqlalchemy.sql import and_, or_
@@ -85,12 +86,13 @@
         self.has_search = True
 
     def _update_background_audio(self, song, item):
+        # TODO: To Path objects
         song.media_files = []
         for i, bga in enumerate(item.background_audio):
-            dest_file = os.path.join(
-                AppLocation.get_section_data_path(self.plugin.name), 'audio', str(song.id), os.path.split(bga)[1])
-            check_directory_exists(os.path.split(dest_file)[0])
-            shutil.copyfile(os.path.join(AppLocation.get_section_data_path('servicemanager'), bga), dest_file)
+            dest_file = str(
+                AppLocation.get_section_data_path(self.plugin.name) / 'audio' / str(song.id) / os.path.split(bga)[1])
+            check_directory_exists(Path(os.path.split(dest_file)[0]))
+            shutil.copyfile(str(AppLocation.get_section_data_path('servicemanager') / bga), dest_file)
             song.media_files.append(MediaFile.populate(weight=i, file_name=dest_file))
         self.plugin.manager.save_object(song, True)
 
@@ -533,13 +535,14 @@
                                                                       'copy', 'For song cloning'))
             # Copy audio files from the old to the new song
             if len(old_song.media_files) > 0:
-                save_path = os.path.join(AppLocation.get_section_data_path(self.plugin.name), 'audio', str(new_song.id))
+                save_path = AppLocation.get_section_data_path(self.plugin.name) / 'audio' / str(new_song.id)
                 check_directory_exists(save_path)
                 for media_file in old_song.media_files:
-                    new_media_file_name = os.path.join(save_path, os.path.basename(media_file.file_name))
-                    shutil.copyfile(media_file.file_name, new_media_file_name)
+                    new_media_file_name = save_path / os.path.basename(media_file.file_name)
+                    shutil.copyfile(media_file.file_name, str(new_media_file_name))
                     new_media_file = MediaFile()
-                    new_media_file.file_name = new_media_file_name
+                    # TODO: new_media_file.file_name to Path object
+                    new_media_file.file_name = str(new_media_file_name)
                     new_media_file.type = media_file.type
                     new_media_file.weight = media_file.weight
                     new_song.media_files.append(new_media_file)

=== modified file 'openlp/plugins/songs/lib/openlyricsexport.py'
--- openlp/plugins/songs/lib/openlyricsexport.py	2016-12-31 11:01:36 +0000
+++ openlp/plugins/songs/lib/openlyricsexport.py	2017-06-19 07:33:18 +0000
@@ -24,7 +24,7 @@
 format.
 """
 import logging
-import os
+from pathlib2 import Path
 
 from lxml import etree
 
@@ -41,6 +41,12 @@
     def __init__(self, parent, songs, save_path):
         """
         Initialise the export.
+        
+        :param save_path: The directory to save the exported songs in
+        :type save_path: Path
+
+        :return: None
+        :rtype: None
         """
         log.debug('initialise OpenLyricsExport')
         self.parent = parent
@@ -68,15 +74,15 @@
                                                    author=', '.join([author.display_name for author in song.authors]))
             filename = clean_filename(filename)
             # Ensure the filename isn't too long for some filesystems
-            filename_with_ext = '{name}.xml'.format(name=filename[0:250 - len(self.save_path)])
+            path_length = len(str(self.save_path))
+            filename_with_ext = '{name}.xml'.format(name=filename[0:250 - path_length])
             # Make sure we're not overwriting an existing file
             conflicts = 0
-            while os.path.exists(os.path.join(self.save_path, filename_with_ext)):
+            while (self.save_path / filename_with_ext).exists():
                 conflicts += 1
-                filename_with_ext = '{name}-{extra}.xml'.format(name=filename[0:247 - len(self.save_path)],
-                                                                extra=conflicts)
+                filename_with_ext = '{name}-{extra}.xml'.format(name=filename[0:247 - path_length], extra=conflicts)
             # Pass a file object, because lxml does not cope with some special
             # characters in the path (see lp:757673 and lp:744337).
-            tree.write(open(os.path.join(self.save_path, filename_with_ext), 'wb'), encoding='utf-8',
-                       xml_declaration=True, pretty_print=True)
+            with (self.save_path / filename_with_ext).open('wb') as out_file:
+                tree.write(out_file, encoding='utf-8', xml_declaration=True, pretty_print=True)
         return True

=== modified file 'openlp/plugins/songs/reporting.py'
--- openlp/plugins/songs/reporting.py	2016-12-31 11:01:36 +0000
+++ openlp/plugins/songs/reporting.py	2017-06-19 07:33:18 +0000
@@ -24,8 +24,9 @@
 """
 import csv
 import logging
+from pathlib2 import Path
 
-from PyQt5 import QtWidgets
+from patches.pyqt5patches import PQFileDialog
 
 from openlp.core.common import Registry, translate
 from openlp.core.lib.ui import critical_error_message_box
@@ -42,13 +43,13 @@
     """
     main_window = Registry().get('main_window')
     plugin = Registry().get('songs').plugin
-    report_file_name, filter_used = QtWidgets.QFileDialog.getSaveFileName(
+    report_file_path, filter_used = PQFileDialog.getSaveFileName(
         main_window,
         translate('SongPlugin.ReportSongList', 'Save File'),
-        translate('SongPlugin.ReportSongList', 'song_extract.csv'),
+        Path(translate('SongPlugin.ReportSongList', 'song_extract.csv')),
         translate('SongPlugin.ReportSongList', 'CSV format (*.csv)'))
 
-    if not report_file_name:
+    if report_file_path is None:
         main_window.error_message(
             translate('SongPlugin.ReportSongList', 'Output Path Not Selected'),
             translate('SongPlugin.ReportSongList', 'You have not set a valid output location for your '
@@ -56,44 +57,42 @@
                                                    'on your computer.')
         )
         return
-    if not report_file_name.endswith('csv'):
-        report_file_name += '.csv'
-    file_handle = None
+    report_file_path.with_suffix('.csv')
     Registry().get('application').set_busy_cursor()
     try:
-        file_handle = open(report_file_name, 'wt')
-        fieldnames = ('Title', 'Alternative Title', 'Copyright', 'Author(s)', 'Song Book', 'Topic')
-        writer = csv.DictWriter(file_handle, fieldnames=fieldnames, quoting=csv.QUOTE_ALL)
-        headers = dict((n, n) for n in fieldnames)
-        writer.writerow(headers)
-        song_list = plugin.manager.get_all_objects(Song)
-        for song in song_list:
-            author_list = []
-            for author_song in song.authors_songs:
-                author_list.append(author_song.author.display_name)
-            author_string = ' | '.join(author_list)
-            book_list = []
-            for book_song in song.songbook_entries:
-                if hasattr(book_song, 'entry') and book_song.entry:
-                    book_list.append('{name} #{entry}'.format(name=book_song.songbook.name, entry=book_song.entry))
-            book_string = ' | '.join(book_list)
-            topic_list = []
-            for topic_song in song.topics:
-                if hasattr(topic_song, 'name'):
-                    topic_list.append(topic_song.name)
-            topic_string = ' | '.join(topic_list)
-            writer.writerow({'Title': song.title,
-                             'Alternative Title': song.alternate_title,
-                             'Copyright': song.copyright,
-                             'Author(s)': author_string,
-                             'Song Book': book_string,
-                             'Topic': topic_string})
-        Registry().get('application').set_normal_cursor()
-        main_window.information_message(
-            translate('SongPlugin.ReportSongList', 'Report Creation'),
-            translate('SongPlugin.ReportSongList',
-                      'Report \n{name} \nhas been successfully created. ').format(name=report_file_name)
-        )
+        with report_file_path.open('wt') as export_file:
+            fieldnames = ('Title', 'Alternative Title', 'Copyright', 'Author(s)', 'Song Book', 'Topic')
+            writer = csv.DictWriter(export_file, fieldnames=fieldnames, quoting=csv.QUOTE_ALL)
+            headers = dict((n, n) for n in fieldnames)
+            writer.writerow(headers)
+            song_list = plugin.manager.get_all_objects(Song)
+            for song in song_list:
+                author_list = []
+                for author_song in song.authors_songs:
+                    author_list.append(author_song.author.display_name)
+                author_string = ' | '.join(author_list)
+                book_list = []
+                for book_song in song.songbook_entries:
+                    if hasattr(book_song, 'entry') and book_song.entry:
+                        book_list.append('{name} #{entry}'.format(name=book_song.songbook.name, entry=book_song.entry))
+                book_string = ' | '.join(book_list)
+                topic_list = []
+                for topic_song in song.topics:
+                    if hasattr(topic_song, 'name'):
+                        topic_list.append(topic_song.name)
+                topic_string = ' | '.join(topic_list)
+                writer.writerow({'Title': song.title,
+                                 'Alternative Title': song.alternate_title,
+                                 'Copyright': song.copyright,
+                                 'Author(s)': author_string,
+                                 'Song Book': book_string,
+                                 'Topic': topic_string})
+            Registry().get('application').set_normal_cursor()
+            main_window.information_message(
+                translate('SongPlugin.ReportSongList', 'Report Creation'),
+                translate('SongPlugin.ReportSongList',
+                          'Report \n{name} \nhas been successfully created. ').format(name=report_file_path)
+            )
     except OSError as ose:
         Registry().get('application').set_normal_cursor()
         log.exception('Failed to write out song usage records')
@@ -101,6 +100,3 @@
                                    translate('SongPlugin.ReportSongList',
                                              'An error occurred while extracting: {error}'
                                              ).format(error=ose.strerror))
-    finally:
-        if file_handle:
-            file_handle.close()

=== modified file 'openlp/plugins/songs/songsplugin.py'
--- openlp/plugins/songs/songsplugin.py	2017-06-04 09:52:15 +0000
+++ openlp/plugins/songs/songsplugin.py	2017-06-19 07:33:18 +0000
@@ -67,7 +67,6 @@
     'songs/songselect username': '',
     'songs/songselect password': '',
     'songs/songselect searches': '',
-    'songs/enable chords': True,
     'songs/chord notation': 'english',  # Can be english, german or neo-latin
     'songs/mainview chords': False,
     'songs/disable chords import': False,
@@ -88,6 +87,7 @@
         super(SongsPlugin, self).__init__('songs', __default_settings__, SongMediaItem, SongsTab)
         self.manager = Manager('songs', init_schema, upgrade_mod=upgrade)
         self.weight = -10
+        # TODO: icon_path to Path object?
         self.icon_path = ':/plugins/plugin_songs.png'
         self.icon = build_icon(self.icon_path)
         self.songselect_form = None
@@ -334,7 +334,7 @@
         progress.forceShow()
         self.application.process_events()
         for db in song_dbs:
-            importer = OpenLPSongImport(self.manager, filename=db)
+            importer = OpenLPSongImport(self.manager, file_path=db)
             importer.do_import(progress)
             self.application.process_events()
         progress.setValue(song_count)

=== modified file 'openlp/plugins/songusage/forms/songusagedetaildialog.py'
--- openlp/plugins/songusage/forms/songusagedetaildialog.py	2017-06-09 06:06:49 +0000
+++ openlp/plugins/songusage/forms/songusagedetaildialog.py	2017-06-19 07:33:18 +0000
@@ -19,7 +19,6 @@
 # with this program; if not, write to the Free Software Foundation, Inc., 59  #
 # Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
 ###############################################################################
-
 from PyQt5 import QtCore, QtWidgets
 
 from openlp.core.common import translate

=== 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-06-19 07:33:18 +0000
@@ -19,10 +19,9 @@
 # with this program; if not, write to the Free Software Foundation, Inc., 59  #
 # Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
 ###############################################################################
-
 import logging
-import os
 
+from patches.utils import path_to_str, str_to_path
 from PyQt5 import QtCore, QtWidgets
 from sqlalchemy.sql import and_
 
@@ -55,13 +54,19 @@
         """
         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 = Settings().path_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)
+    def on_report_path_edit_path_changed(self, new_path):
+        """
+        Handle the `pathEditChanged` signal from report_path_edit
+        
+        :param new_path: Path to the new video
+        :type new_path: Path
+        
+        :return: None
+        :rtype: None
+        """
+        Settings().set_path_value(self.plugin.settings_section + '/last directory export', new_path)
 
     def accept(self):
         """
@@ -87,29 +92,25 @@
             SongUsageItem, and_(SongUsageItem.usagedate >= self.from_date_calendar.selectedDate().toPyDate(),
                                 SongUsageItem.usagedate < self.to_date_calendar.selectedDate().toPyDate()),
             [SongUsageItem.usagedate, SongUsageItem.usagetime])
-        report_file_name = os.path.join(path, file_name)
-        file_handle = None
+        report_file_name = path / file_name
         try:
-            file_handle = open(report_file_name, 'wb')
-            for instance in usage:
-                record = ('\"{date}\",\"{time}\",\"{title}\",\"{copyright}\",\"{ccli}\",\"{authors}\",'
-                          '\"{name}\",\"{source}\"\n').format(date=instance.usagedate, time=instance.usagetime,
-                                                              title=instance.title, copyright=instance.copyright,
-                                                              ccli=instance.ccl_number, authors=instance.authors,
-                                                              name=instance.plugin_name, source=instance.source)
-                file_handle.write(record.encode('utf-8'))
-            self.main_window.information_message(
-                translate('SongUsagePlugin.SongUsageDetailForm', 'Report Creation'),
-                translate('SongUsagePlugin.SongUsageDetailForm',
-                          'Report \n{name} \nhas been successfully created. ').format(name=report_file_name)
-            )
+            with report_file_name.open('wb') as file_handle:
+                for instance in usage:
+                    record = ('\"{date}\",\"{time}\",\"{title}\",\"{copyright}\",\"{ccli}\",\"{authors}\",'
+                              '\"{name}\",\"{source}\"\n').format(date=instance.usagedate, time=instance.usagetime,
+                                                                  title=instance.title, copyright=instance.copyright,
+                                                                  ccli=instance.ccl_number, authors=instance.authors,
+                                                                  name=instance.plugin_name, source=instance.source)
+                    file_handle.write(record.encode('utf-8'))
+                self.main_window.information_message(
+                    translate('SongUsagePlugin.SongUsageDetailForm', 'Report Creation'),
+                    translate('SongUsagePlugin.SongUsageDetailForm',
+                              'Report \n{name} \nhas been successfully created. ').format(name=report_file_name)
+                )
         except OSError as ose:
             log.exception('Failed to write out song usage records')
             critical_error_message_box(translate('SongUsagePlugin.SongUsageDetailForm', 'Report Creation Failed'),
                                        translate('SongUsagePlugin.SongUsageDetailForm',
                                                  'An error occurred while creating the report: {error}'
                                                  ).format(error=ose.strerror))
-        finally:
-            if file_handle:
-                file_handle.close()
         self.close()

=== modified file 'tests/functional/openlp_core_common/test_applocation.py'
--- tests/functional/openlp_core_common/test_applocation.py	2017-04-24 05:17:55 +0000
+++ tests/functional/openlp_core_common/test_applocation.py	2017-06-19 07:33:18 +0000
@@ -24,8 +24,9 @@
 """
 import copy
 import os
+from pathlib2 import Path
 from unittest import TestCase
-from unittest.mock import patch
+from unittest.mock import MagicMock, patch
 
 from openlp.core.common import AppLocation, get_frozen_path
 
@@ -64,56 +65,51 @@
         """
         Test the AppLocation.get_data_path() method when a custom location is set in the settings
         """
-        with patch('openlp.core.common.applocation.Settings') as mocked_class,\
-                patch('openlp.core.common.applocation.os') as mocked_os:
-            # GIVEN: A mocked out Settings class which returns a custom data location
-            mocked_settings = mocked_class.return_value
-            mocked_settings.contains.return_value = True
-            mocked_settings.value.return_value.toString.return_value = 'custom/dir'
-            mocked_os.path.normpath.return_value = 'custom/dir'
+        # GIVEN: A mocked out Settings class which returns a custom data location
+        mocked_settings_instance = MagicMock(
+            **{'contains.return_value': True, 'path_value.return_value': Path('custom', 'dir')})
+        with patch('openlp.core.common.applocation.Settings', return_value=mocked_settings_instance):
 
             # WHEN: we call AppLocation.get_data_path()
             data_path = AppLocation.get_data_path()
 
             # THEN: the mocked Settings methods were called and the value returned was our set up value
-            mocked_settings.contains.assert_called_with('advanced/data path')
-            mocked_settings.value.assert_called_with('advanced/data path')
-            self.assertEqual('custom/dir', data_path, 'Result should be "custom/dir"')
+            mocked_settings_instance.contains.assert_called_with('advanced/data path')
+            mocked_settings_instance.path_value.assert_called_with('advanced/data path')
+            self.assertEqual(Path('custom', 'dir'), data_path, 'Result should be "custom/dir"')
 
     def test_get_files_no_section_no_extension(self):
         """
         Test the AppLocation.get_files() method with no parameters passed.
         """
-        with patch('openlp.core.common.AppLocation.get_data_path') as mocked_get_data_path, \
-                patch('openlp.core.common.applocation.os.listdir') as mocked_listdir:
-            # GIVEN: Our mocked modules/methods.
-            mocked_get_data_path.return_value = 'test/dir'
-            mocked_listdir.return_value = copy.deepcopy(FILE_LIST)
+        # GIVEN: Our mocked modules/methods.
+        with patch.object(Path, 'glob', return_value=[Path('/dir/file5.mp3'), Path('/dir/file6.mp3')]) as mocked_glob, \
+                patch('openlp.core.common.AppLocation.get_data_path', return_value=Path('/dir')):
 
             # When: Get the list of files.
             result = AppLocation.get_files()
 
+            # Then: Check if the section parameter was used correctly, and the glob argument was passed.
+            mocked_glob.assert_called_once_with('*')
+
             # Then: check if the file lists are identical.
-            self.assertListEqual(FILE_LIST, result, 'The file lists should be identical.')
+            self.assertListEqual([Path('file5.mp3'), Path('file6.mp3')], result, 'The file lists should be identical.')
 
     def test_get_files(self):
         """
         Test the AppLocation.get_files() method with all parameters passed.
         """
-        with patch('openlp.core.common.AppLocation.get_data_path') as mocked_get_data_path, \
-                patch('openlp.core.common.applocation.os.listdir') as mocked_listdir:
-            # GIVEN: Our mocked modules/methods.
-            mocked_get_data_path.return_value = os.path.join('test', 'dir')
-            mocked_listdir.return_value = copy.deepcopy(FILE_LIST)
+        # GIVEN: Our mocked modules/methods.
+        with patch.object(Path, 'glob', return_value=[Path('/dir/section/file5.mp3'), Path('/dir/section/file6.mp3')]) \
+                as mocked_glob, \
+                patch('openlp.core.common.AppLocation.get_data_path', return_value=Path('/dir')):
 
             # When: Get the list of files.
             result = AppLocation.get_files('section', '.mp3')
 
-            # Then: Check if the section parameter was used correctly.
-            mocked_listdir.assert_called_with(os.path.join('test', 'dir', 'section'))
-
-            # Then: check if the file lists are identical.
-            self.assertListEqual(['file5.mp3', 'file6.mp3'], result, 'The file lists should be identical.')
+            # Then: The section parameter was used correctly, and the glob argument was passed..
+            mocked_glob.assert_called_once_with('*.mp3')
+            self.assertListEqual([Path('file5.mp3'), Path('file6.mp3')], result, 'The file lists should be identical.')
 
     def test_get_section_data_path(self):
         """
@@ -122,15 +118,15 @@
         with patch('openlp.core.common.AppLocation.get_data_path') as mocked_get_data_path, \
                 patch('openlp.core.common.applocation.check_directory_exists') as mocked_check_directory_exists:
             # GIVEN: A mocked out AppLocation.get_data_path()
-            mocked_get_data_path.return_value = os.path.join('test', 'dir')
+            mocked_get_data_path.return_value = Path('test', 'dir')
             mocked_check_directory_exists.return_value = True
 
             # WHEN: we call AppLocation.get_data_path()
             data_path = AppLocation.get_section_data_path('section')
 
             # THEN: check that all the correct methods were called, and the result is correct
-            mocked_check_directory_exists.assert_called_with(os.path.join('test', 'dir', 'section'))
-            self.assertEqual(os.path.join('test', 'dir', 'section'), data_path, 'Result should be "test/dir/section"')
+            mocked_check_directory_exists.assert_called_with(Path('test', 'dir', 'section'))
+            self.assertEqual(Path('test', 'dir', 'section'), data_path, 'Result should be "test/dir/section"')
 
     def test_get_directory_for_app_dir(self):
         """
@@ -138,13 +134,13 @@
         """
         # GIVEN: A mocked out _get_frozen_path function
         with patch('openlp.core.common.applocation.get_frozen_path') as mocked_get_frozen_path:
-            mocked_get_frozen_path.return_value = os.path.join('app', 'dir')
+            mocked_get_frozen_path.return_value = Path('app', 'dir')
 
             # WHEN: We call AppLocation.get_directory
             directory = AppLocation.get_directory(AppLocation.AppDir)
 
             # THEN: check that the correct directory is returned
-            self.assertEqual(os.path.join('app', 'dir'), directory, 'Directory should be "app/dir"')
+            self.assertEqual(Path('app', 'dir'), directory, 'Directory should be "app/dir"')
 
     def test_get_directory_for_plugins_dir(self):
         """
@@ -157,7 +153,7 @@
                 patch('openlp.core.common.applocation.sys') as mocked_sys:
             mocked_abspath.return_value = os.path.join('plugins', 'dir')
             mocked_split.return_value = ['openlp']
-            mocked_get_frozen_path.return_value = os.path.join('plugins', 'dir')
+            mocked_get_frozen_path.return_value = Path('dir')
             mocked_sys.frozen = 1
             mocked_sys.argv = ['openlp']
 
@@ -165,7 +161,7 @@
             directory = AppLocation.get_directory(AppLocation.PluginsDir)
 
             # THEN: The correct directory should be returned
-            self.assertEqual(os.path.join('plugins', 'dir'), directory, 'Directory should be "plugins/dir"')
+            self.assertEqual(Path('dir', 'plugins'), directory, 'Directory should be "dir/plugins"')
 
     def test_get_frozen_path_in_unfrozen_app(self):
         """

=== modified file 'tests/functional/openlp_core_common/test_common.py'
--- tests/functional/openlp_core_common/test_common.py	2017-05-15 10:15:32 +0000
+++ tests/functional/openlp_core_common/test_common.py	2017-06-19 07:33:18 +0000
@@ -22,7 +22,7 @@
 """
 Functional tests to test the AppLocation class and related methods.
 """
-from pathlib import Path
+from pathlib2 import Path
 from unittest import TestCase
 from unittest.mock import MagicMock, call, patch
 
@@ -35,51 +35,77 @@
     """
     A test suite to test out various functions in the openlp.core.common module.
     """
-    def test_check_directory_exists(self):
-        """
-        Test the check_directory_exists() function
-        """
-        with patch('openlp.core.lib.os.path.exists') as mocked_exists, \
-                patch('openlp.core.lib.os.makedirs') as mocked_makedirs:
-            # GIVEN: A directory to check and a mocked out os.makedirs and os.path.exists
-            directory_to_check = 'existing/directory'
+    def test_check_directory_exists_dir_exists(self):
+        """
+        Test the check_directory_exists() function when the path already exists
+        """
+        # GIVEN: A `Path` to check with patched out mkdir and exists methods
+        with patch.object(Path, 'exists') as mocked_exists, \
+                patch.object(Path, 'mkdir') as mocked_mkdir, \
+                patch('openlp.core.common.log'):
 
-            # WHEN: os.path.exists returns True and we check to see if the directory exists
+            # WHEN: `check_directory_exists` is called and the path exists
             mocked_exists.return_value = True
-            check_directory_exists(directory_to_check)
-
-            # THEN: Only os.path.exists should have been called
-            mocked_exists.assert_called_with(directory_to_check)
-            self.assertIsNot(mocked_makedirs.called, 'os.makedirs should not have been called')
-
-            # WHEN: os.path.exists returns False and we check the directory exists
+            check_directory_exists(Path('existing', 'directory'))
+
+            # THEN: The function should not attempt to create the directory
+            mocked_exists.assert_called_with()
+            self.assertFalse(mocked_mkdir.called)
+
+    def test_check_directory_exists_dir_doesnt_exists(self):
+        """
+        Test the check_directory_exists() function when the path does not already exist
+        """
+        # GIVEN: A `Path` to check with patched out mkdir and exists methods
+        with patch.object(Path, 'exists') as mocked_exists, \
+                patch.object(Path, 'mkdir') as mocked_mkdir, \
+                patch('openlp.core.common.log'):
+
+            # WHEN: `check_directory_exists` is called and the path does not exist
             mocked_exists.return_value = False
-            check_directory_exists(directory_to_check)
-
-            # THEN: Both the mocked functions should have been called
-            mocked_exists.assert_called_with(directory_to_check)
-            mocked_makedirs.assert_called_with(directory_to_check)
-
-            # WHEN: os.path.exists raises an IOError
+            check_directory_exists(Path('existing', 'directory'))
+
+            # THEN: The directory should have been created
+            mocked_exists.assert_called_with()
+            mocked_mkdir.assert_called_with(parents=True)
+
+    def test_check_directory_exists_dir_ioerror(self):
+        """
+        Test the check_directory_exists() when an IOError is raised
+        """
+        # GIVEN: A `Path` to check with patched out mkdir and exists methods
+        with patch.object(Path, 'exists') as mocked_exists, \
+                patch.object(Path, 'mkdir'), \
+                patch('openlp.core.common.log') as mocked_logger:
+
+            # WHEN: An IOError is raised when checking the if the path exists.
             mocked_exists.side_effect = IOError()
-            check_directory_exists(directory_to_check)
-
-            # THEN: We shouldn't get an exception though the mocked exists has been called
-            mocked_exists.assert_called_with(directory_to_check)
+            check_directory_exists(Path('existing', 'directory'))
+
+            # THEN: The Error should have been logged
+            mocked_logger.exception.assert_called_once_with('failed to check if directory exists or create directory')
+
+    def test_check_directory_exists_dir_value_error(self):
+        """
+        Test the check_directory_exists() when an error other than IOError is raised
+        """
+        # GIVEN: A `Path` to check with patched out mkdir and exists methods
+        with patch.object(Path, 'exists') as mocked_exists, \
+                patch.object(Path, 'mkdir'), \
+                patch('openlp.core.common.log'):
 
             # WHEN: Some other exception is raised
             mocked_exists.side_effect = ValueError()
 
-            # THEN: check_directory_exists raises an exception
-            mocked_exists.assert_called_with(directory_to_check)
-            self.assertRaises(ValueError, check_directory_exists, directory_to_check)
+            # THEN: `check_directory_exists` raises an exception
+            self.assertRaises(ValueError, check_directory_exists, Path('existing', 'directory'))
 
     def test_extension_loader_no_files_found(self):
         """
         Test the `extension_loader` function when no files are found
         """
         # GIVEN: A mocked `Path.glob` method which does not match any files
-        with patch('openlp.core.common.AppLocation.get_directory', return_value='/app/dir/openlp'), \
+        with patch('openlp.core.common.AppLocation.get_directory', return_value=Path('/app/dir/openlp')), \
                 patch.object(common.Path, 'glob', return_value=[]), \
                 patch('openlp.core.common.importlib.import_module') as mocked_import_module:
 
@@ -94,7 +120,7 @@
         Test the `extension_loader` function when it successfully finds and loads some files
         """
         # GIVEN: A mocked `Path.glob` method which returns a list of files
-        with patch('openlp.core.common.AppLocation.get_directory', return_value='/app/dir/openlp'), \
+        with patch('openlp.core.common.AppLocation.get_directory', return_value=Path('/app/dir/openlp')), \
                 patch.object(common.Path, 'glob', return_value=[Path('/app/dir/openlp/import_dir/file1.py'),
                                                                 Path('/app/dir/openlp/import_dir/file2.py'),
                                                                 Path('/app/dir/openlp/import_dir/file3.py'),
@@ -113,7 +139,7 @@
         Test the `extension_loader` function when `SourceFileLoader` raises a `ImportError`
         """
         # GIVEN: A mocked `import_module` which raises an `ImportError`
-        with patch('openlp.core.common.AppLocation.get_directory', return_value='/app/dir/openlp'), \
+        with patch('openlp.core.common.AppLocation.get_directory', return_value=Path('/app/dir/openlp')), \
                 patch.object(common.Path, 'glob', return_value=[Path('/app/dir/openlp/import_dir/file1.py')]), \
                 patch('openlp.core.common.importlib.import_module', side_effect=ImportError()), \
                 patch('openlp.core.common.log') as mocked_logger:
@@ -129,7 +155,7 @@
         Test the `extension_loader` function when `import_module` raises a `ImportError`
         """
         # GIVEN: A mocked `SourceFileLoader` which raises an `OSError`
-        with patch('openlp.core.common.AppLocation.get_directory', return_value='/app/dir/openlp'), \
+        with patch('openlp.core.common.AppLocation.get_directory', return_value=Path('/app/dir/openlp')), \
                 patch.object(common.Path, 'glob', return_value=[Path('/app/dir/openlp/import_dir/file1.py')]), \
                 patch('openlp.core.common.importlib.import_module', side_effect=OSError()), \
                 patch('openlp.core.common.log') as mocked_logger:

=== modified file 'tests/functional/openlp_core_common/test_httputils.py'
--- tests/functional/openlp_core_common/test_httputils.py	2017-04-24 05:17:55 +0000
+++ tests/functional/openlp_core_common/test_httputils.py	2017-06-19 07:33:18 +0000
@@ -25,6 +25,7 @@
 import os
 import tempfile
 import socket
+from pathlib2 import Path
 from unittest import TestCase
 from unittest.mock import MagicMock, patch
 
@@ -267,7 +268,7 @@
         mocked_urlopen.side_effect = socket.timeout()
 
         # WHEN: Attempt to retrieve a file
-        url_get_file(MagicMock(), url='http://localhost/test', f_path=self.tempfile)
+        url_get_file(MagicMock(), url='http://localhost/test', f_path=Path(self.tempfile))
 
         # THEN: socket.timeout should have been caught
         # NOTE: Test is if $tmpdir/tempfile is still there, then test fails since ftw deletes bad downloaded files

=== modified file 'tests/functional/openlp_core_common/test_init.py'
--- tests/functional/openlp_core_common/test_init.py	2017-04-24 05:17:55 +0000
+++ tests/functional/openlp_core_common/test_init.py	2017-06-19 07:33:18 +0000
@@ -24,6 +24,7 @@
 """
 import os
 from io import BytesIO
+from pathlib2 import Path
 from unittest import TestCase
 from unittest.mock import MagicMock, PropertyMock, call, patch
 
@@ -305,43 +306,48 @@
         """
         Test the delete_file function when it successfully deletes a file
         """
-        # GIVEN: A mocked os which returns True when os.path.exists is called
-        with patch('openlp.core.common.os', **{'path.exists.return_value': False}):
-
-            # WHEN: Calling delete_file with a file path
-            result = delete_file('path/file.ext')
-
-            # THEN: delete_file should return True
+        # GIVEN: A patched `exists` methods on the Path object, which returns True
+        with patch.object(Path, 'exists', return_value=True), \
+                patch.object(Path, 'unlink') as mocked_unlink:
+
+            # WHEN: Calling `delete_file` with a file path
+            result = delete_file(Path('path', 'file.ext'))
+
+            # THEN: The file should be deleted, and `delete_file` should return True
+            self.assertTrue(mocked_unlink.called)
             self.assertTrue(result, 'delete_file should return True when it successfully deletes a file')
 
     def test_delete_file_path_no_file_exists(self):
         """
-        Test the delete_file function when the file to remove does not exist
+        Test the `delete_file` function when the file to remove does not exist
         """
-        # GIVEN: A mocked os which returns False when os.path.exists is called
-        with patch('openlp.core.common.os', **{'path.exists.return_value': False}):
-
-            # WHEN: Calling delete_file with a file path
-            result = delete_file('path/file.ext')
-
-            # THEN: delete_file should return True
+        # GIVEN: A patched `exists` methods on the Path object, which returns False
+        with patch.object(Path, 'exists', return_value=False), \
+                patch.object(Path, 'unlink') as mocked_unlink:
+
+            # WHEN: Calling `delete_file with` a file path
+            result = delete_file(Path('path', 'file.ext'))
+
+            # THEN: The function should not attempt to delete the file and it should return True
+            self.assertFalse(mocked_unlink.called)
             self.assertTrue(result, 'delete_file should return True when the file doesnt exist')
 
     def test_delete_file_path_exception(self):
         """
-        Test the delete_file function when os.remove raises an exception
+        Test the delete_file function when an exception is raised
         """
-        # GIVEN: A mocked os which returns True when os.path.exists is called and raises an OSError when os.remove is
+        # GIVEN: A test `Path` object with a patched exists method which raises an OSError
         #       called.
-        with patch('openlp.core.common.os', **{'path.exists.return_value': True, 'path.exists.side_effect': OSError}), \
+        with patch.object(Path, 'exists') as mocked_exists, \
                 patch('openlp.core.common.log') as mocked_log:
-
-            # WHEN: Calling delete_file with a file path
-            result = delete_file('path/file.ext')
-
-            # THEN: delete_file should log and exception and return False
-            self.assertEqual(mocked_log.exception.call_count, 1)
-            self.assertFalse(result, 'delete_file should return False when os.remove raises an OSError')
+            mocked_exists.side_effect = OSError
+
+            # WHEN: Calling delete_file with a the test Path object
+            result = delete_file(Path('path', 'file.ext'))
+
+            # THEN: The exception should be logged and `delete_file` should return False
+            self.assertTrue(mocked_log.exception.called)
+            self.assertFalse(result, 'delete_file should return False when an OSError is raised')
 
     def test_get_file_name_encoding_done_test(self):
         """
@@ -349,17 +355,17 @@
         """
         # GIVEN: A mocked UniversalDetector instance with done attribute set to True after first iteration
         with patch('openlp.core.common.UniversalDetector') as mocked_universal_detector, \
-                patch('builtins.open', return_value=BytesIO(b"data" * 260)) as mocked_open:
+                patch('openlp.core.common.Path.open', return_value=BytesIO(b"data" * 260)) as mocked_open:
             encoding_result = {'encoding': 'UTF-8', 'confidence': 0.99}
             mocked_universal_detector_inst = MagicMock(result=encoding_result)
             type(mocked_universal_detector_inst).done = PropertyMock(side_effect=[False, True])
             mocked_universal_detector.return_value = mocked_universal_detector_inst
 
             # WHEN: Calling get_file_encoding
-            result = get_file_encoding('file name')
+            result = get_file_encoding(Path('file name'))
 
             # THEN: The feed method of UniversalDetector should only br called once before returning a result
-            mocked_open.assert_called_once_with('file name', 'rb')
+            mocked_open.assert_called_once_with('rb')
             self.assertEqual(mocked_universal_detector_inst.feed.mock_calls, [call(b"data" * 256)])
             mocked_universal_detector_inst.close.assert_called_once_with()
             self.assertEqual(result, encoding_result)
@@ -371,17 +377,17 @@
         # GIVEN: A mocked UniversalDetector instance which isn't set to done and a mocked open, with 1040 bytes of test
         #       data (enough to run the iterator twice)
         with patch('openlp.core.common.UniversalDetector') as mocked_universal_detector, \
-                patch('builtins.open', return_value=BytesIO(b"data" * 260)) as mocked_open:
+                patch('openlp.core.common.Path.open', return_value=BytesIO(b"data" * 260)) as mocked_open:
             encoding_result = {'encoding': 'UTF-8', 'confidence': 0.99}
             mocked_universal_detector_inst = MagicMock(mock=mocked_universal_detector,
                                                        **{'done': False, 'result': encoding_result})
             mocked_universal_detector.return_value = mocked_universal_detector_inst
 
             # WHEN: Calling get_file_encoding
-            result = get_file_encoding('file name')
+            result = get_file_encoding(Path('file name'))
 
             # THEN: The feed method of UniversalDetector should have been called twice before returning a result
-            mocked_open.assert_called_once_with('file name', 'rb')
+            mocked_open.assert_called_once_with('rb')
             self.assertEqual(mocked_universal_detector_inst.feed.mock_calls, [call(b"data" * 256), call(b"data" * 4)])
             mocked_universal_detector_inst.close.assert_called_once_with()
             self.assertEqual(result, encoding_result)
@@ -397,7 +403,7 @@
                 patch('openlp.core.common.log') as mocked_log:
 
             # WHEN: Calling get_file_encoding
-            result = get_file_encoding('file name')
+            result = get_file_encoding(Path('file name'))
 
             # THEN: log.exception should be called and get_file_encoding should return None
             mocked_log.exception.assert_called_once_with('Error detecting file encoding')

=== modified file 'tests/functional/openlp_core_lib/test_db.py'
--- tests/functional/openlp_core_lib/test_db.py	2017-04-24 05:17:55 +0000
+++ tests/functional/openlp_core_lib/test_db.py	2017-06-19 07:33:18 +0000
@@ -23,6 +23,7 @@
 Package to test the openlp.core.lib package.
 """
 import os
+from pathlib2 import Path
 from unittest import TestCase
 from unittest.mock import patch, MagicMock
 
@@ -112,10 +113,10 @@
         # GIVEN: Mocked out AppLocation class and delete_file method, a test plugin name and a db location
         with patch('openlp.core.lib.db.AppLocation') as MockedAppLocation, \
                 patch('openlp.core.lib.db.delete_file') as mocked_delete_file:
-            MockedAppLocation.get_section_data_path.return_value = 'test-dir'
+            MockedAppLocation.get_section_data_path.return_value = Path('test-dir')
             mocked_delete_file.return_value = True
             test_plugin = 'test'
-            test_location = os.path.join('test-dir', test_plugin)
+            test_location = Path('test-dir', test_plugin)
 
             # WHEN: delete_database is run without a database file
             result = delete_database(test_plugin)
@@ -132,11 +133,11 @@
         # GIVEN: Mocked out AppLocation class and delete_file method, a test plugin name and a db location
         with patch('openlp.core.lib.db.AppLocation') as MockedAppLocation, \
                 patch('openlp.core.lib.db.delete_file') as mocked_delete_file:
-            MockedAppLocation.get_section_data_path.return_value = 'test-dir'
+            MockedAppLocation.get_section_data_path.return_value = Path('test-dir')
             mocked_delete_file.return_value = False
             test_plugin = 'test'
             test_db_file = 'mydb.sqlite'
-            test_location = os.path.join('test-dir', test_db_file)
+            test_location = Path('test-dir', test_db_file)
 
             # WHEN: delete_database is run without a database file
             result = delete_database(test_plugin, test_db_file)

=== modified file 'tests/functional/openlp_core_lib/test_image_manager.py'
--- tests/functional/openlp_core_lib/test_image_manager.py	2017-04-24 05:17:55 +0000
+++ tests/functional/openlp_core_lib/test_image_manager.py	2017-06-19 07:33:18 +0000
@@ -24,6 +24,7 @@
 """
 import os
 import time
+from pathlib2 import Path
 from threading import Lock
 from unittest import TestCase
 from unittest.mock import patch
@@ -63,7 +64,7 @@
         Test the Image Manager setup basic functionality
         """
         # GIVEN: the an image add to the image manager
-        full_path = os.path.normpath(os.path.join(TEST_PATH, 'church.jpg'))
+        full_path = Path(TEST_PATH, 'church.jpg')
         self.image_manager.add_image(full_path, 'church.jpg', None)
 
         # WHEN the image is retrieved
@@ -89,7 +90,7 @@
         Test the Image Manager with dimensions
         """
         # GIVEN: add an image with specific dimensions
-        full_path = os.path.normpath(os.path.join(TEST_PATH, 'church.jpg'))
+        full_path = Path(TEST_PATH, 'church.jpg')
         self.image_manager.add_image(full_path, 'church.jpg', None, 80, 80)
 
         # WHEN: the image is retrieved
@@ -132,8 +133,8 @@
 
             # WHEN: Add the images. Then get the lock (=queue can not be processed).
             self.lock.acquire()
-            self.image_manager.add_image(TEST_PATH, image1, None)
-            self.image_manager.add_image(TEST_PATH, image2, None)
+            self.image_manager.add_image(Path(TEST_PATH), image1, None)
+            self.image_manager.add_image(Path(TEST_PATH), image2, None)
 
             # THEN: All images have been added to the queue, and only the first image is not be in the list anymore, but
             #  is being processed (see mocked methods/functions).
@@ -145,13 +146,13 @@
                              "image2's priority should be 'Priority.Normal'")
 
             # WHEN: Add more images.
-            self.image_manager.add_image(TEST_PATH, image3, None)
-            self.image_manager.add_image(TEST_PATH, image4, None)
+            self.image_manager.add_image(Path(TEST_PATH), image3, None)
+            self.image_manager.add_image(Path(TEST_PATH), image4, None)
             # Allow the queue to process.
             self.lock.release()
             # Request some "data".
-            image_bytes = self.image_manager.get_image_bytes(TEST_PATH, image4)
-            image_object = self.image_manager.get_image(TEST_PATH, image3)
+            image_bytes = self.image_manager.get_image_bytes(Path(TEST_PATH), image4)
+            image_object = self.image_manager.get_image(Path(TEST_PATH), image3)
             # Now the mocked methods/functions do not have to sleep anymore.
             self.sleep_time = 0
             # Wait for the queue to finish.
@@ -178,7 +179,7 @@
 
         :param image: The name of the image. E. g. ``image1``
         """
-        return self.image_manager._cache[(TEST_PATH, image, -1, -1)].priority
+        return self.image_manager._cache[(Path(TEST_PATH), image, -1, -1)].priority
 
     def mocked_resize_image(self, *args):
         """

=== 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-06-19 07:33:18 +0000
@@ -24,6 +24,7 @@
 """
 import os
 from datetime import datetime, timedelta
+from pathlib2 import Path
 from unittest import TestCase
 from unittest.mock import MagicMock, patch
 
@@ -37,6 +38,150 @@
 TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'resources'))
 
 
+class TestCreateThumb(TestCase):
+    def remove_thumb(self):
+        """
+        A utility method to remove the created thumbnail
+        """
+        try:
+            self.thumb_path.unlink()
+        except:
+            pass
+
+    def setUp(self):
+        self.image_path = Path(TEST_PATH, 'church.jpg')
+        self.thumb_path = Path(TEST_PATH, 'church_thumb.jpg')
+        # Remove the thumb so that the test actually tests if the thumb will be created. Maybe it was not deleted in the
+        # last test.
+        self.remove_thumb()
+        # Only continue when the thumb does not exist.
+        self.assertFalse(self.thumb_path.exists(), 'Test was not run, because the thumb already exists.')
+
+    def tearDown(self):
+        # Remove the thumb so that the test actually tests if the thumb will be created.
+        self.remove_thumb()
+
+    def test_create_thumb_with_size(self):
+        """
+        Test the create_thumb() function with a given size.
+        """
+        # GIVEN: An image to create a thumb of.
+        thumb_size = QtCore.QSize(10, 20)
+
+        # WHEN: Create the thumb.
+        icon = create_thumb(self.image_path, self.thumb_path, size=thumb_size)
+
+        # THEN: Check if the thumb was created and scaled to the given size.
+        self.assertTrue(self.thumb_path.exists(), 'Test was not ran, because the thumb already exists')
+        self.assertIsInstance(icon, QtGui.QIcon, 'The icon should be a QIcon')
+        self.assertFalse(icon.isNull(), 'The icon should not be null')
+        self.assertEqual(thumb_size, QtGui.QImageReader(str(self.thumb_path)).size(),
+                         'The thumb should have the given size')
+
+    def test_create_thumb_no_size(self):
+        """
+        Test the create_thumb() function with no size specified.
+        """
+        # GIVEN: An image to create a thumb of.
+        expected_size = QtCore.QSize(63, 88)
+
+        # WHEN: Create the thumb.
+        icon = create_thumb(self.image_path, self.thumb_path)
+
+        # THEN: Check if the thumb was created, retaining its aspect ratio.
+        self.assertTrue(self.thumb_path.exists(), 'Test was not ran, because the thumb already exists')
+        self.assertIsInstance(icon, QtGui.QIcon, 'The icon should be a QIcon')
+        self.assertFalse(icon.isNull(), 'The icon should not be null')
+        self.assertEqual(expected_size, QtGui.QImageReader(str(self.thumb_path)).size(),
+                         'The thumb should have the given size')
+
+    def test_create_thumb_invalid_size(self):
+        """
+        Test the create_thumb() function with invalid size specified.
+        """
+        # GIVEN: An image to create a thumb of.
+        thumb_size = QtCore.QSize(-1, -1)
+        expected_size = QtCore.QSize(63, 88)
+
+        # WHEN: Create the thumb.
+        icon = create_thumb(self.image_path, self.thumb_path, size=thumb_size)
+
+        # THEN: Check if the thumb was created, retaining its aspect ratio.
+        self.assertTrue(self.thumb_path.exists(), 'Test was not ran, because the thumb already exists')
+        self.assertIsInstance(icon, QtGui.QIcon, 'The icon should be a QIcon')
+        self.assertFalse(icon.isNull(), 'The icon should not be null')
+        self.assertEqual(expected_size, QtGui.QImageReader(str(self.thumb_path)).size(),
+                         'The thumb should have the given size')
+
+    def test_create_thumb_width_only(self):
+        """
+        Test the create_thumb() function with a size of only width specified.
+        """
+        # GIVEN: An image to create a thumb of.
+        thumb_size = QtCore.QSize(100, -1)
+        expected_size = QtCore.QSize(100, 137)
+
+        # WHEN: Create the thumb.
+        icon = create_thumb(self.image_path, self.thumb_path, size=thumb_size)
+
+        # THEN: Check if the thumb was created, retaining its aspect ratio.
+        self.assertTrue(self.thumb_path.exists(), 'Test was not ran, because the thumb already exists')
+        self.assertIsInstance(icon, QtGui.QIcon, 'The icon should be a QIcon')
+        self.assertFalse(icon.isNull(), 'The icon should not be null')
+        self.assertEqual(expected_size, QtGui.QImageReader(str(self.thumb_path)).size(),
+                         'The thumb should have the given size')
+
+    def test_create_thumb_height_only(self):
+        """
+        Test the create_thumb() function with a size of only height specified.
+        """
+        # GIVEN: An image to create a thumb of.
+        thumb_size = QtCore.QSize(-1, 100)
+        expected_size = QtCore.QSize(72, 100)
+
+        # WHEN: Create the thumb.
+        icon = create_thumb(self.image_path, self.thumb_path, size=thumb_size)
+
+        # THEN: Check if the thumb was created, retaining its aspect ratio.
+        self.assertTrue(self.thumb_path.exists(), 'Test was not ran, because the thumb already exists')
+        self.assertIsInstance(icon, QtGui.QIcon, 'The icon should be a QIcon')
+        self.assertFalse(icon.isNull(), 'The icon should not be null')
+        self.assertEqual(expected_size, QtGui.QImageReader(str(self.thumb_path)).size(),
+                         'The thumb should have the given size')
+
+    def test_create_thumb_empty_img(self):
+        """
+        Test the create_thumb() function with a size of only height specified.
+        """
+        # GIVEN: An image to create a thumb of.
+        thumb_size = QtCore.QSize(-1, 100)
+        expected_size_1 = QtCore.QSize(88, 88)
+        expected_size_2 = QtCore.QSize(100, 100)
+
+        # WHEN: Create the thumb.
+        with patch('openlp.core.lib.QtGui.QImageReader.size') as mocked_size:
+            mocked_size.return_value = QtCore.QSize(0, 0)
+            icon = create_thumb(self.image_path, self.thumb_path, size=None)
+
+        # THEN: Check if the thumb was created with aspect ratio of 1.
+        self.assertTrue(self.thumb_path.exists(), 'Test was not ran, because the thumb already exists')
+        self.assertIsInstance(icon, QtGui.QIcon, 'The icon should be a QIcon')
+        self.assertFalse(icon.isNull(), 'The icon should not be null')
+        self.assertEqual(expected_size_1, QtGui.QImageReader(str(self.thumb_path)).size(),
+                         'The thumb should have the given size')
+
+        # WHEN: Create the thumb.
+        with patch('openlp.core.lib.QtGui.QImageReader.size') as mocked_size:
+            mocked_size.return_value = QtCore.QSize(0, 0)
+            icon = create_thumb(self.image_path, self.thumb_path, size=thumb_size)
+
+        # THEN: Check if the thumb was created with aspect ratio of 1.
+        self.assertIsInstance(icon, QtGui.QIcon, 'The icon should be a QIcon')
+        self.assertFalse(icon.isNull(), 'The icon should not be null')
+        self.assertEqual(expected_size_2, QtGui.QImageReader(str(self.thumb_path)).size(),
+                         'The thumb should have the given size')
+
+
 class TestLib(TestCase):
 
     def test_str_to_bool_with_bool_true(self):
@@ -149,35 +294,34 @@
         """
         Test the get_text_file_string() function when a file does not exist
         """
-        with patch('openlp.core.lib.os.path.isfile') as mocked_isfile:
-            # GIVEN: A mocked out isfile which returns true, and a text file name
-            filename = 'testfile.txt'
-            mocked_isfile.return_value = False
+        # GIVEN: A patched is_file which returns False, and a file path
+        with patch.object(Path, 'is_file', return_value=False):
+            file_path = Path('testfile.txt')
 
             # WHEN: get_text_file_string is called
-            result = get_text_file_string(filename)
+            result = get_text_file_string(file_path)
 
             # THEN: The result should be False
-            mocked_isfile.assert_called_with(filename)
+            file_path.is_file.assert_called_with()
             self.assertFalse(result, 'False should be returned if no file exists')
 
     def test_get_text_file_string_read_error(self):
         """
         Test the get_text_file_string() method when a read error happens
         """
-        with patch('openlp.core.lib.os.path.isfile') as mocked_isfile, \
-                patch('openlp.core.lib.open', create=True) as mocked_open:
-            # GIVEN: A mocked-out open() which raises an exception and isfile returns True
-            filename = 'testfile.txt'
-            mocked_isfile.return_value = True
-            mocked_open.side_effect = IOError()
+        # GIVEN: A patched open which raises an exception and is_file which returns True
+        with patch.object(Path, 'is_file'), \
+                patch.object(Path, 'open'):
+            file_path = Path('testfile.txt')
+            file_path.is_file.return_value = True
+            file_path.open.side_effect = IOError()
 
             # WHEN: get_text_file_string is called
-            result = get_text_file_string(filename)
+            result = get_text_file_string(file_path)
 
             # THEN: None should be returned
-            mocked_isfile.assert_called_with(filename)
-            mocked_open.assert_called_with(filename, 'r', encoding='utf-8')
+            file_path.is_file.assert_called_once_with()
+            file_path.open.assert_called_once_with('r', encoding='utf-8')
             self.assertIsNone(result, 'None should be returned if the file cannot be opened')
 
     def test_get_text_file_string_decode_error(self):
@@ -271,227 +415,6 @@
             self.assertEqual('base64mock', result, 'The result should be the return value of the mocked out '
                                                    'base64 method')
 
-    def test_create_thumb_with_size(self):
-        """
-        Test the create_thumb() function with a given size.
-        """
-        # GIVEN: An image to create a thumb of.
-        image_path = os.path.join(TEST_PATH, 'church.jpg')
-        thumb_path = os.path.join(TEST_PATH, 'church_thumb.jpg')
-        thumb_size = QtCore.QSize(10, 20)
-
-        # Remove the thumb so that the test actually tests if the thumb will be created. Maybe it was not deleted in the
-        # last test.
-        try:
-            os.remove(thumb_path)
-        except:
-            pass
-
-        # Only continue when the thumb does not exist.
-        self.assertFalse(os.path.exists(thumb_path), 'Test was not run, because the thumb already exists.')
-
-        # WHEN: Create the thumb.
-        icon = create_thumb(image_path, thumb_path, size=thumb_size)
-
-        # THEN: Check if the thumb was created and scaled to the given size.
-        self.assertTrue(os.path.exists(thumb_path), 'Test was not ran, because the thumb already exists')
-        self.assertIsInstance(icon, QtGui.QIcon, 'The icon should be a QIcon')
-        self.assertFalse(icon.isNull(), 'The icon should not be null')
-        self.assertEqual(thumb_size, QtGui.QImageReader(thumb_path).size(), 'The thumb should have the given size')
-
-        # Remove the thumb so that the test actually tests if the thumb will be created.
-        try:
-            os.remove(thumb_path)
-        except:
-            pass
-
-    def test_create_thumb_no_size(self):
-        """
-        Test the create_thumb() function with no size specified.
-        """
-        # GIVEN: An image to create a thumb of.
-        image_path = os.path.join(TEST_PATH, 'church.jpg')
-        thumb_path = os.path.join(TEST_PATH, 'church_thumb.jpg')
-        expected_size = QtCore.QSize(63, 88)
-
-        # Remove the thumb so that the test actually tests if the thumb will be created. Maybe it was not deleted in the
-        # last test.
-        try:
-            os.remove(thumb_path)
-        except:
-            pass
-
-        # Only continue when the thumb does not exist.
-        self.assertFalse(os.path.exists(thumb_path), 'Test was not run, because the thumb already exists.')
-
-        # WHEN: Create the thumb.
-        icon = create_thumb(image_path, thumb_path)
-
-        # THEN: Check if the thumb was created, retaining its aspect ratio.
-        self.assertTrue(os.path.exists(thumb_path), 'Test was not ran, because the thumb already exists')
-        self.assertIsInstance(icon, QtGui.QIcon, 'The icon should be a QIcon')
-        self.assertFalse(icon.isNull(), 'The icon should not be null')
-        self.assertEqual(expected_size, QtGui.QImageReader(thumb_path).size(), 'The thumb should have the given size')
-
-        # Remove the thumb so that the test actually tests if the thumb will be created.
-        try:
-            os.remove(thumb_path)
-        except:
-            pass
-
-    def test_create_thumb_invalid_size(self):
-        """
-        Test the create_thumb() function with invalid size specified.
-        """
-        # GIVEN: An image to create a thumb of.
-        image_path = os.path.join(TEST_PATH, 'church.jpg')
-        thumb_path = os.path.join(TEST_PATH, 'church_thumb.jpg')
-        thumb_size = QtCore.QSize(-1, -1)
-        expected_size = QtCore.QSize(63, 88)
-
-        # Remove the thumb so that the test actually tests if the thumb will be created. Maybe it was not deleted in the
-        # last test.
-        try:
-            os.remove(thumb_path)
-        except:
-            pass
-
-        # Only continue when the thumb does not exist.
-        self.assertFalse(os.path.exists(thumb_path), 'Test was not run, because the thumb already exists.')
-
-        # WHEN: Create the thumb.
-        icon = create_thumb(image_path, thumb_path, size=thumb_size)
-
-        # THEN: Check if the thumb was created, retaining its aspect ratio.
-        self.assertTrue(os.path.exists(thumb_path), 'Test was not ran, because the thumb already exists')
-        self.assertIsInstance(icon, QtGui.QIcon, 'The icon should be a QIcon')
-        self.assertFalse(icon.isNull(), 'The icon should not be null')
-        self.assertEqual(expected_size, QtGui.QImageReader(thumb_path).size(), 'The thumb should have the given size')
-
-        # Remove the thumb so that the test actually tests if the thumb will be created.
-        try:
-            os.remove(thumb_path)
-        except:
-            pass
-
-    def test_create_thumb_width_only(self):
-        """
-        Test the create_thumb() function with a size of only width specified.
-        """
-        # GIVEN: An image to create a thumb of.
-        image_path = os.path.join(TEST_PATH, 'church.jpg')
-        thumb_path = os.path.join(TEST_PATH, 'church_thumb.jpg')
-        thumb_size = QtCore.QSize(100, -1)
-        expected_size = QtCore.QSize(100, 137)
-
-        # Remove the thumb so that the test actually tests if the thumb will be created. Maybe it was not deleted in the
-        # last test.
-        try:
-            os.remove(thumb_path)
-        except:
-            pass
-
-        # Only continue when the thumb does not exist.
-        self.assertFalse(os.path.exists(thumb_path), 'Test was not run, because the thumb already exists.')
-
-        # WHEN: Create the thumb.
-        icon = create_thumb(image_path, thumb_path, size=thumb_size)
-
-        # THEN: Check if the thumb was created, retaining its aspect ratio.
-        self.assertTrue(os.path.exists(thumb_path), 'Test was not ran, because the thumb already exists')
-        self.assertIsInstance(icon, QtGui.QIcon, 'The icon should be a QIcon')
-        self.assertFalse(icon.isNull(), 'The icon should not be null')
-        self.assertEqual(expected_size, QtGui.QImageReader(thumb_path).size(), 'The thumb should have the given size')
-
-        # Remove the thumb so that the test actually tests if the thumb will be created.
-        try:
-            os.remove(thumb_path)
-        except:
-            pass
-
-    def test_create_thumb_height_only(self):
-        """
-        Test the create_thumb() function with a size of only height specified.
-        """
-        # GIVEN: An image to create a thumb of.
-        image_path = os.path.join(TEST_PATH, 'church.jpg')
-        thumb_path = os.path.join(TEST_PATH, 'church_thumb.jpg')
-        thumb_size = QtCore.QSize(-1, 100)
-        expected_size = QtCore.QSize(72, 100)
-
-        # Remove the thumb so that the test actually tests if the thumb will be created. Maybe it was not deleted in the
-        # last test.
-        try:
-            os.remove(thumb_path)
-        except:
-            pass
-
-        # Only continue when the thumb does not exist.
-        self.assertFalse(os.path.exists(thumb_path), 'Test was not run, because the thumb already exists.')
-
-        # WHEN: Create the thumb.
-        icon = create_thumb(image_path, thumb_path, size=thumb_size)
-
-        # THEN: Check if the thumb was created, retaining its aspect ratio.
-        self.assertTrue(os.path.exists(thumb_path), 'Test was not ran, because the thumb already exists')
-        self.assertIsInstance(icon, QtGui.QIcon, 'The icon should be a QIcon')
-        self.assertFalse(icon.isNull(), 'The icon should not be null')
-        self.assertEqual(expected_size, QtGui.QImageReader(thumb_path).size(), 'The thumb should have the given size')
-
-        # Remove the thumb so that the test actually tests if the thumb will be created.
-        try:
-            os.remove(thumb_path)
-        except:
-            pass
-
-    def test_create_thumb_empty_img(self):
-        """
-        Test the create_thumb() function with a size of only height specified.
-        """
-        # GIVEN: An image to create a thumb of.
-        image_path = os.path.join(TEST_PATH, 'church.jpg')
-        thumb_path = os.path.join(TEST_PATH, 'church_thumb.jpg')
-        thumb_size = QtCore.QSize(-1, 100)
-        expected_size_1 = QtCore.QSize(88, 88)
-        expected_size_2 = QtCore.QSize(100, 100)
-
-        # Remove the thumb so that the test actually tests if the thumb will be created. Maybe it was not deleted in the
-        # last test.
-        try:
-            os.remove(thumb_path)
-        except:
-            pass
-
-        # Only continue when the thumb does not exist.
-        self.assertFalse(os.path.exists(thumb_path), 'Test was not run, because the thumb already exists.')
-
-        # WHEN: Create the thumb.
-        with patch('openlp.core.lib.QtGui.QImageReader.size') as mocked_size:
-            mocked_size.return_value = QtCore.QSize(0, 0)
-            icon = create_thumb(image_path, thumb_path, size=None)
-
-        # THEN: Check if the thumb was created with aspect ratio of 1.
-        self.assertTrue(os.path.exists(thumb_path), 'Test was not ran, because the thumb already exists')
-        self.assertIsInstance(icon, QtGui.QIcon, 'The icon should be a QIcon')
-        self.assertFalse(icon.isNull(), 'The icon should not be null')
-        self.assertEqual(expected_size_1, QtGui.QImageReader(thumb_path).size(), 'The thumb should have the given size')
-
-        # WHEN: Create the thumb.
-        with patch('openlp.core.lib.QtGui.QImageReader.size') as mocked_size:
-            mocked_size.return_value = QtCore.QSize(0, 0)
-            icon = create_thumb(image_path, thumb_path, size=thumb_size)
-
-        # THEN: Check if the thumb was created with aspect ratio of 1.
-        self.assertIsInstance(icon, QtGui.QIcon, 'The icon should be a QIcon')
-        self.assertFalse(icon.isNull(), 'The icon should not be null')
-        self.assertEqual(expected_size_2, QtGui.QImageReader(thumb_path).size(), 'The thumb should have the given size')
-
-        # Remove the thumb so that the test actually tests if the thumb will be created.
-        try:
-            os.remove(thumb_path)
-        except:
-            pass
-
     def test_check_item_selected_true(self):
         """
         Test that the check_item_selected() function returns True when there are selected indexes
@@ -595,62 +518,45 @@
         """
         Test the validate_thumb() function when the thumbnail does not exist
         """
-        # GIVEN: A mocked out os module, with path.exists returning False, and fake paths to a file and a thumb
-        with patch('openlp.core.lib.os') as mocked_os:
-            file_path = 'path/to/file'
-            thumb_path = 'path/to/thumb'
-            mocked_os.path.exists.return_value = False
-
-            # WHEN: we run the validate_thumb() function
-            result = validate_thumb(file_path, thumb_path)
-
-            # THEN: we should have called a few functions, and the result should be False
-            mocked_os.path.exists.assert_called_with(thumb_path)
-            assert result is False, 'The result should be False'
+        # GIVEN: A mocked out thumb_path, which `doesn't exist`, and fake file_path
+        file_path = MagicMock()
+        thumb_path = MagicMock(**{'exists.return_value': False})
+
+        # WHEN: we run the validate_thumb() function
+        result = validate_thumb(file_path, thumb_path)
+
+        # THEN: The function should not compare time stamps and False should be returned
+        self.assertFalse(file_path.stat.called)
+        self.assertFalse(result, 'The result should be False')
 
     def test_validate_thumb_file_exists_and_newer(self):
         """
         Test the validate_thumb() function when the thumbnail exists and has a newer timestamp than the file
         """
-        # GIVEN: A mocked out os module, functions rigged to work for us, and fake paths to a file and a thumb
-        with patch('openlp.core.lib.os') as mocked_os:
-            file_path = 'path/to/file'
-            thumb_path = 'path/to/thumb'
-            file_mocked_stat = MagicMock()
-            file_mocked_stat.st_mtime = datetime.now()
-            thumb_mocked_stat = MagicMock()
-            thumb_mocked_stat.st_mtime = datetime.now() + timedelta(seconds=10)
-            mocked_os.path.exists.return_value = True
-            mocked_os.stat.side_effect = [file_mocked_stat, thumb_mocked_stat]
-
-            # WHEN: we run the validate_thumb() function
-
-            # THEN: we should have called a few functions, and the result should be True
-            # mocked_os.path.exists.assert_called_with(thumb_path)
+        # GIVEN: Mocked file_path and thumb_path which return different values fo the modified times
+        file_path = MagicMock(**{'stat.return_value': MagicMock(st_mtime=10)})
+        thumb_path = MagicMock(**{'exists.return_value': True, 'stat.return_value': MagicMock(st_mtime=11)})
+
+        # WHEN: we run the validate_thumb() function
+        result = validate_thumb(file_path, thumb_path)
+
+        # THEN: `validate_thumb` should return True
+        self.assertTrue(result)
 
     def test_validate_thumb_file_exists_and_older(self):
         """
         Test the validate_thumb() function when the thumbnail exists but is older than the file
         """
-        # GIVEN: A mocked out os module, functions rigged to work for us, and fake paths to a file and a thumb
-        with patch('openlp.core.lib.os') as mocked_os:
-            file_path = 'path/to/file'
-            thumb_path = 'path/to/thumb'
-            file_mocked_stat = MagicMock()
-            file_mocked_stat.st_mtime = datetime.now()
-            thumb_mocked_stat = MagicMock()
-            thumb_mocked_stat.st_mtime = datetime.now() - timedelta(seconds=10)
-            mocked_os.path.exists.return_value = True
-            mocked_os.stat.side_effect = lambda fname: file_mocked_stat if fname == file_path else thumb_mocked_stat
-
-            # WHEN: we run the validate_thumb() function
-            result = validate_thumb(file_path, thumb_path)
-
-            # THEN: we should have called a few functions, and the result should be False
-            mocked_os.path.exists.assert_called_with(thumb_path)
-            mocked_os.stat.assert_any_call(file_path)
-            mocked_os.stat.assert_any_call(thumb_path)
-            assert result is False, 'The result should be False'
+        # GIVEN: Mocked file_path and thumb_path which return different values fo the modified times
+        file_path = MagicMock(**{'stat.return_value': MagicMock(st_mtime=10)})
+        thumb_path = MagicMock(**{'exists.return_value': True, 'stat.return_value': MagicMock(st_mtime=9)})
+
+        # WHEN: we run the validate_thumb() function
+        result = validate_thumb(file_path, thumb_path)
+
+        # THEN: `validate_thumb` should return False
+        thumb_path.stat.assert_called_once_with()
+        self.assertFalse(result, 'The result should be False')
 
     def test_resize_thumb(self):
         """
@@ -677,36 +583,28 @@
         """
         Test the create_separated_list function using the Qt provided method
         """
-        with patch('openlp.core.lib.Qt') as mocked_qt, \
-                patch('openlp.core.lib.QtCore.QLocale.createSeparatedList') as mocked_createSeparatedList:
-            # GIVEN: A list of strings and the mocked Qt module.
-            mocked_qt.PYQT_VERSION_STR = '4.9'
-            mocked_qt.qVersion.return_value = '4.8'
-            mocked_createSeparatedList.return_value = 'Author 1, Author 2, and Author 3'
-            string_list = ['Author 1', 'Author 2', 'Author 3']
-
-            # WHEN: We get a string build from the entries it the list and a separator.
-            string_result = create_separated_list(string_list)
-
-            # THEN: We should have "Author 1, Author 2, and Author 3"
-            self.assertEqual(string_result, 'Author 1, Author 2 and Author 3', 'The string should be "Author 1, '
-                             'Author 2, and Author 3".')
+        # GIVEN: A list of strings and the mocked Qt module.
+        string_list = ['Author 1', 'Author 2', 'Author 3']
+
+        # WHEN: We get a string build from the entries it the list and a separator.
+        string_result = create_separated_list(string_list)
+
+        # THEN: We should have "Author 1, Author 2, and Author 3"
+        self.assertEqual(string_result, 'Author 1, Author 2 and Author 3', 'The string should be "Author 1, '
+                         'Author 2, and Author 3".')
 
     def test_create_separated_list_empty_list(self):
         """
         Test the create_separated_list function with an empty list
         """
-        with patch('openlp.core.lib.Qt') as mocked_qt:
-            # GIVEN: An empty list and the mocked Qt module.
-            mocked_qt.PYQT_VERSION_STR = '4.8'
-            mocked_qt.qVersion.return_value = '4.7'
-            string_list = []
-
-            # WHEN: We get a string build from the entries it the list and a separator.
-            string_result = create_separated_list(string_list)
-
-            # THEN: We shoud have an emptry string.
-            self.assertEqual(string_result, '', 'The string sould be empty.')
+        # GIVEN: An empty list.
+        string_list = []
+
+        # WHEN: We get a string build from the entries it the list and a separator.
+        string_result = create_separated_list(string_list)
+
+        # THEN: We shoud have an emptry string.
+        self.assertEqual(string_result, '', 'The string sould be empty.')
 
     def test_create_separated_list_with_one_item(self):
         """

=== modified file 'tests/functional/openlp_core_lib/test_serviceitem.py'
--- tests/functional/openlp_core_lib/test_serviceitem.py	2017-05-11 20:24:20 +0000
+++ tests/functional/openlp_core_lib/test_serviceitem.py	2017-06-19 07:33:18 +0000
@@ -23,6 +23,7 @@
 Package to test the openlp.core.lib package.
 """
 import os
+from pathlib2 import Path
 from unittest import TestCase
 from unittest.mock import MagicMock, patch
 
@@ -122,14 +123,15 @@
         # GIVEN: A new service item and a mocked add icon function
         image_name = 'image_1.jpg'
         test_file = os.path.join(TEST_PATH, image_name)
-        frame_array = {'path': test_file, 'title': image_name}
+        test_path = Path(TEST_PATH, image_name)
+        frame_array = {'path': test_path, 'title': image_name}
 
         service_item = ServiceItem(None)
         service_item.add_icon = MagicMock()
 
         # WHEN: adding an image from a saved Service and mocked exists
         line = convert_file_service_item(TEST_PATH, 'serviceitem_image_1.osj')
-        with patch('openlp.core.ui.servicemanager.os.path.exists') as mocked_exists,\
+        with patch('openlp.core.ui.servicemanager.Path.exists') as mocked_exists,\
                 patch('openlp.core.lib.serviceitem.AppLocation.get_section_data_path') as \
                 mocked_get_section_data_path:
             mocked_exists.return_value = True
@@ -138,11 +140,11 @@
 
         # THEN: We should get back a valid service item
         self.assertTrue(service_item.is_valid, 'The new service item should be valid')
-        self.assertEqual(os.path.normpath(test_file), os.path.normpath(service_item.get_rendered_frame(0)),
+        self.assertEqual(test_path, service_item.get_rendered_frame(0),
                          'The first frame should match the path to the image')
         self.assertEqual(frame_array, service_item.get_frames()[0],
                          'The return should match frame array1')
-        self.assertEqual(test_file, service_item.get_frame_path(0),
+        self.assertEqual(test_path, service_item.get_frame_path(0),
                          'The frame path should match the full path to the image')
         self.assertEqual(image_name, service_item.get_frame_title(0),
                          'The frame title should match the image name')
@@ -165,8 +167,8 @@
         # GIVEN: A new service item and a mocked add icon function
         image_name1 = 'image_1.jpg'
         image_name2 = 'image_2.jpg'
-        test_file1 = os.path.normpath(os.path.join('/home/openlp', image_name1))
-        test_file2 = os.path.normpath(os.path.join('/home/openlp', image_name2))
+        test_file1 = Path('/home', 'openlp', image_name1)
+        test_file2 = Path('/home', 'openlp', image_name2)
         frame_array1 = {'path': test_file1, 'title': image_name1}
         frame_array2 = {'path': test_file2, 'title': image_name2}
 
@@ -180,7 +182,7 @@
         line = convert_file_service_item(TEST_PATH, 'serviceitem_image_2.osj')
         line2 = convert_file_service_item(TEST_PATH, 'serviceitem_image_2.osj', 1)
 
-        with patch('openlp.core.ui.servicemanager.os.path.exists') as mocked_exists, \
+        with patch('openlp.core.ui.servicemanager.Path.exists') as mocked_exists, \
                 patch('openlp.core.lib.serviceitem.AppLocation.get_section_data_path') as \
                 mocked_get_section_data_path:
             mocked_exists.return_value = True
@@ -232,7 +234,7 @@
         image = MagicMock()
         display_title = 'DisplayTitle'
         notes = 'Note1\nNote2\n'
-        frame = {'title': presentation_name, 'image': image, 'path': TEST_PATH,
+        frame = {'title': presentation_name, 'image': image, 'path': Path(TEST_PATH),
                  'display_title': display_title, 'notes': notes}
 
         # WHEN: adding presentation to service_item
@@ -250,7 +252,7 @@
         service_item = ServiceItem(None)
         image_name = 'test.img'
         image = MagicMock()
-        frame = {'title': image_name, 'image': image, 'path': TEST_PATH,
+        frame = {'title': image_name, 'image': image, 'path': Path(TEST_PATH),
                  'display_title': None, 'notes': None}
 
         # WHEN: adding image to service_item
@@ -260,14 +262,14 @@
         self.assertEqual(service_item.service_item_type, ServiceItemType.Command, 'It should be a Command')
         self.assertEqual(service_item.get_frames()[0], frame, 'Frames should match')
 
-    @patch(u'openlp.core.lib.serviceitem.ServiceItem.image_manager')
+    @patch('openlp.core.lib.serviceitem.ServiceItem.image_manager')
     @patch('openlp.core.lib.serviceitem.AppLocation.get_section_data_path')
     def test_add_from_command_for_a_presentation_thumb(self, mocked_get_section_data_path, mocked_image_manager):
         """
         Test the Service Item - adding a presentation, updating the thumb path & adding the thumb to image_manager
         """
         # GIVEN: A service item, a mocked AppLocation and presentation data
-        mocked_get_section_data_path.return_value = os.path.join('mocked', 'section', 'path')
+        mocked_get_section_data_path.return_value = Path('mocked', 'section', 'path')
         service_item = ServiceItem(None)
         service_item.add_capability(ItemCapabilities.HasThumbnails)
         service_item.has_original_files = False
@@ -276,10 +278,9 @@
         thumb = os.path.join('tmp', 'test', 'thumb.png')
         display_title = 'DisplayTitle'
         notes = 'Note1\nNote2\n'
-        expected_thumb_path = os.path.join('mocked', 'section', 'path', 'thumbnails',
-                                           md5_hash(os.path.join(TEST_PATH, presentation_name).encode('utf-8')),
-                                           'thumb.png')
-        frame = {'title': presentation_name, 'image': expected_thumb_path, 'path': TEST_PATH,
+        expected_thumb_path = Path('mocked', 'section', 'path', 'thumbnails',
+                                   md5_hash(os.path.join(TEST_PATH, presentation_name).encode('utf-8')), 'thumb.png')
+        frame = {'title': presentation_name, 'image': expected_thumb_path, 'path': Path(TEST_PATH),
                  'display_title': display_title, 'notes': notes}
 
         # WHEN: adding presentation to service_item

=== modified file 'tests/functional/openlp_core_lib/test_theme.py'
--- tests/functional/openlp_core_lib/test_theme.py	2017-05-24 20:04:48 +0000
+++ tests/functional/openlp_core_lib/test_theme.py	2017-06-19 07:33:18 +0000
@@ -22,8 +22,9 @@
 """
 Package to test the openlp.core.lib.theme package.
 """
+import os
+from pathlib2 import Path
 from unittest import TestCase
-import os
 
 from openlp.core.lib.theme import Theme
 
@@ -82,13 +83,13 @@
         theme.theme_name = 'MyBeautifulTheme   '
         theme.background_filename = '    video.mp4'
         theme.background_type = 'video'
-        path = os.path.expanduser('~')
+        path = Path().home()
 
         # WHEN: Theme.extend_image_filename is run
         theme.extend_image_filename(path)
 
         # THEN: The filename of the background should be correct
-        expected_filename = os.path.join(path, 'MyBeautifulTheme', 'video.mp4')
+        expected_filename = Path(path, 'MyBeautifulTheme', 'video.mp4')
         self.assertEqual(expected_filename, theme.background_filename)
         self.assertEqual('MyBeautifulTheme', theme.theme_name)
 

=== modified file 'tests/functional/openlp_core_ui/test_exceptionform.py'
--- tests/functional/openlp_core_ui/test_exceptionform.py	2017-04-24 05:17:55 +0000
+++ tests/functional/openlp_core_ui/test_exceptionform.py	2017-06-19 07:33:18 +0000
@@ -22,11 +22,12 @@
 """
 Package to test the openlp.core.ui.exeptionform package.
 """
-
 import os
 import tempfile
+from pathlib2 import Path
+
 from unittest import TestCase
-from unittest.mock import mock_open, patch
+from unittest.mock import MagicMock, call, mock_open, patch
 
 from openlp.core.common import Registry
 from openlp.core.ui import exceptionform
@@ -141,20 +142,20 @@
         test_form = exceptionform.ExceptionForm()
         test_form.file_attachment = None
 
-        with patch.object(test_form, '_pyuno_import') as mock_pyuno:
-            with patch.object(test_form.exception_text_edit, 'toPlainText') as mock_traceback:
-                with patch.object(test_form.description_text_edit, 'toPlainText') as mock_description:
-                    mock_pyuno.return_value = 'UNO Bridge Test'
-                    mock_traceback.return_value = 'openlp: Traceback Test'
-                    mock_description.return_value = 'Description Test'
+        with patch.object(test_form, '_pyuno_import') as mock_pyuno, \
+                patch.object(test_form.exception_text_edit, 'toPlainText') as mock_traceback, \
+                patch.object(test_form.description_text_edit, 'toPlainText') as mock_description:
+            mock_pyuno.return_value = 'UNO Bridge Test'
+            mock_traceback.return_value = 'openlp: Traceback Test'
+            mock_description.return_value = 'Description Test'
 
-                    # WHEN: on_save_report_button_clicked called
-                    test_form.on_send_report_button_clicked()
+            # WHEN: on_save_report_button_clicked called
+            test_form.on_send_report_button_clicked()
 
         # 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.PQFileDialog.getSaveFileName")
     @patch("openlp.core.ui.exceptionform.Qt")
     def test_on_save_report_button_clicked(self,
                                            mocked_qt,
@@ -181,25 +182,24 @@
         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', ]
-
-        test_form = exceptionform.ExceptionForm()
-        test_form.file_attachment = None
-
-        with patch.object(test_form, '_pyuno_import') as mock_pyuno:
-            with patch.object(test_form.exception_text_edit, 'toPlainText') as mock_traceback:
-                with patch.object(test_form.description_text_edit, 'toPlainText') as mock_description:
-                    with patch("openlp.core.ui.exceptionform.open", mock_open(), create=True) as mocked_open:
-                        mock_pyuno.return_value = 'UNO Bridge Test'
-                        mock_traceback.return_value = 'openlp: Traceback Test'
-                        mock_description.return_value = 'Description Test'
-
-                        # WHEN: on_save_report_button_clicked called
-                        test_form.on_save_report_button_clicked()
+
+        with patch.object(Path, 'open') as mocked_path_open:
+            x = Path('testfile.txt')
+            mocked_save_filename.return_value = x, 'ext'
+
+            test_form = exceptionform.ExceptionForm()
+            test_form.file_attachment = None
+
+            with patch.object(test_form, '_pyuno_import') as mock_pyuno, \
+                    patch.object(test_form.exception_text_edit, 'toPlainText') as mock_traceback, \
+                    patch.object(test_form.description_text_edit, 'toPlainText') as mock_description:
+                mock_pyuno.return_value = 'UNO Bridge Test'
+                mock_traceback.return_value = 'openlp: Traceback Test'
+                mock_description.return_value = 'Description Test'
+
+                # WHEN: on_save_report_button_clicked called
+                test_form.on_save_report_button_clicked()
 
         # THEN: Verify proper calls to save file
         # self.maxDiff = None
-        check_text = "call().write({text})".format(text=MAIL_ITEM_TEXT.__repr__())
-        write_text = "{text}".format(text=mocked_open.mock_calls[1])
-        mocked_open.assert_called_with('testfile.txt', 'w')
-        self.assertEquals(check_text, write_text, "Saved information should match test text")
+        mocked_path_open.assert_has_calls([call().__enter__().write(MAIL_ITEM_TEXT)])

=== modified file 'tests/functional/openlp_core_ui/test_firsttimeform.py'
--- tests/functional/openlp_core_ui/test_firsttimeform.py	2017-04-24 05:17:55 +0000
+++ tests/functional/openlp_core_ui/test_firsttimeform.py	2017-06-19 07:33:18 +0000
@@ -25,6 +25,7 @@
 import os
 import tempfile
 import urllib
+from pathlib2 import Path
 from unittest import TestCase
 from unittest.mock import MagicMock, patch
 
@@ -116,7 +117,7 @@
             mocked_settings.value.return_value = True
             MockedSettings.return_value = mocked_settings
             mocked_gettempdir.return_value = 'temp'
-            expected_temp_path = os.path.join('temp', 'openlp')
+            expected_temp_path = Path('temp', 'openlp')
 
             # WHEN: The set_defaults() method is run
             frw.set_defaults()

=== modified file 'tests/functional/openlp_core_ui/test_servicemanager.py'
--- tests/functional/openlp_core_ui/test_servicemanager.py	2017-04-24 05:17:55 +0000
+++ tests/functional/openlp_core_ui/test_servicemanager.py	2017-06-19 07:33:18 +0000
@@ -22,7 +22,7 @@
 """
 Package to test the openlp.core.ui.slidecontroller package.
 """
-import os
+from pathlib2 import Path
 from unittest import TestCase
 from unittest.mock import MagicMock, patch
 
@@ -44,6 +44,8 @@
         Create the UI
         """
         Registry.create()
+        self.mocked_main_window = MagicMock()
+        Registry().register('main_window', self.mocked_main_window)
 
     def test_initial_service_manager(self):
         """
@@ -631,12 +633,10 @@
         Test that when a PermissionError is raised when trying to save a file, it is handled correctly
         """
         # GIVEN: A service manager, a service to save
-        mocked_main_window = MagicMock()
-        mocked_main_window.service_manager_settings_section = 'servicemanager'
-        Registry().register('main_window', mocked_main_window)
+        self.mocked_main_window.service_manager_settings_section = 'servicemanager'
         Registry().register('application', MagicMock())
         service_manager = ServiceManager(None)
-        service_manager._file_name = os.path.join('temp', 'filename.osz')
+        service_manager._file_name = Path('temp', 'filename.osz')
         service_manager._save_lite = False
         service_manager.service_items = []
         service_manager.service_theme = 'Default'
@@ -660,12 +660,10 @@
         Test that when a PermissionError is raised when trying to save a local file, it is handled correctly
         """
         # GIVEN: A service manager, a service to save
-        mocked_main_window = MagicMock()
-        mocked_main_window.service_manager_settings_section = 'servicemanager'
-        Registry().register('main_window', mocked_main_window)
+        self.mocked_main_window.service_manager_settings_section = 'servicemanager'
         Registry().register('application', MagicMock())
         service_manager = ServiceManager(None)
-        service_manager._file_name = os.path.join('temp', 'filename.osz')
+        service_manager._file_name = Path('temp', 'filename.osz')
         service_manager._save_lite = False
         service_manager.service_items = []
         service_manager.service_theme = 'Default'

=== modified file 'tests/functional/openlp_core_ui/test_slidecontroller.py'
--- tests/functional/openlp_core_ui/test_slidecontroller.py	2017-04-24 05:17:55 +0000
+++ tests/functional/openlp_core_ui/test_slidecontroller.py	2017-06-19 07:33:18 +0000
@@ -736,8 +736,8 @@
         self.assertEqual(0, slide_controller.on_go_live.call_count, 'on_go_live Should have not been called.')
         self.assertEqual(1, slide_controller.on_preview_add_to_service.call_count, 'Should have been called once.')
 
-    @patch(u'openlp.core.ui.slidecontroller.SlideController.image_manager')
-    @patch(u'PyQt5.QtCore.QTimer.singleShot')
+    @patch('openlp.core.ui.slidecontroller.SlideController.image_manager')
+    @patch('PyQt5.QtCore.QTimer.singleShot')
     def test_update_preview_live(self, mocked_singleShot, mocked_image_manager):
         """
         Test that the preview screen is updated with a screen grab for live service items
@@ -779,8 +779,8 @@
                          'Timer to grab_maindisplay should have been called 2 times')
         self.assertEqual(0, mocked_image_manager.get_image.call_count, 'image_manager not be called')
 
-    @patch(u'openlp.core.ui.slidecontroller.SlideController.image_manager')
-    @patch(u'PyQt5.QtCore.QTimer.singleShot')
+    @patch('openlp.core.ui.slidecontroller.SlideController.image_manager')
+    @patch('PyQt5.QtCore.QTimer.singleShot')
     def test_update_preview_pres(self, mocked_singleShot, mocked_image_manager):
         """
         Test that the preview screen is updated with the correct preview for presentation service items

=== 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-06-19 07:33:18 +0000
@@ -22,6 +22,7 @@
 """
 Package to test the openlp.core.ui.themeform package.
 """
+from pathlib2 import Path
 from unittest import TestCase
 from unittest.mock import MagicMock, patch
 
@@ -45,8 +46,8 @@
             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')
+            self.assertEqual(self.instance.theme.background_filename, Path('/new/pat.h'))
             mocked_set_background_page_values.assert_called_once_with()

=== modified file 'tests/functional/openlp_core_ui/test_thememanager.py'
--- tests/functional/openlp_core_ui/test_thememanager.py	2017-06-01 06:18:47 +0000
+++ tests/functional/openlp_core_ui/test_thememanager.py	2017-06-19 07:33:18 +0000
@@ -24,6 +24,7 @@
 """
 import os
 import shutil
+from pathlib2 import Path
 from tempfile import mkdtemp
 from unittest import TestCase
 from unittest.mock import ANY, MagicMock, patch
@@ -57,7 +58,7 @@
         """
         # GIVEN: A new ThemeManager instance.
         theme_manager = ThemeManager()
-        theme_manager.path = os.path.join(TEST_RESOURCES_PATH, 'themes')
+        theme_manager.theme_path = Path(TEST_RESOURCES_PATH, 'themes')
         with patch('zipfile.ZipFile.__init__') as mocked_zipfile_init, \
                 patch('zipfile.ZipFile.write') as mocked_zipfile_write:
             mocked_zipfile_init.return_value = None
@@ -88,27 +89,23 @@
         """
         # GIVEN: A new theme manager instance, with mocked builtins.open, shutil.copyfile,
         #        theme, check_directory_exists and thememanager-attributes.
-        with patch('builtins.open') as mocked_open, \
-                patch('openlp.core.ui.thememanager.shutil.copyfile') as mocked_copyfile, \
-                patch('openlp.core.ui.thememanager.check_directory_exists') as mocked_check_directory_exists:
-            mocked_open.return_value = MagicMock()
+        with patch('openlp.core.ui.thememanager.shutil.copyfile') as mocked_copyfile, \
+                patch('openlp.core.ui.thememanager.check_directory_exists'):
             theme_manager = ThemeManager(None)
             theme_manager.old_background_image = None
             theme_manager.generate_and_save_image = MagicMock()
-            theme_manager.path = ''
+            theme_manager.theme_path = MagicMock()
             mocked_theme = MagicMock()
             mocked_theme.theme_name = 'themename'
             mocked_theme.extract_formatted_xml = MagicMock()
             mocked_theme.extract_formatted_xml.return_value = 'fake_theme_xml'.encode()
 
             # WHEN: Calling _write_theme with path to the same image, but the path written slightly different
-            file_name1 = os.path.join(TEST_RESOURCES_PATH, 'church.jpg')
-            # Do replacement from end of string to avoid problems with path start
-            file_name2 = file_name1[::-1].replace(os.sep, os.sep + os.sep, 2)[::-1]
-            theme_manager._write_theme(mocked_theme, file_name1, file_name2)
+            file_name1 = Path(TEST_RESOURCES_PATH, 'church.jpg')
+            theme_manager._write_theme(mocked_theme, file_name1, file_name1)
 
             # THEN: The mocked_copyfile should not have been called
-            self.assertFalse(mocked_copyfile.called, 'shutil.copyfile should not be called')
+            self.assertFalse(mocked_copyfile.called, 'copyfile should not be called')
 
     def test_write_theme_diff_images(self):
         """
@@ -116,27 +113,23 @@
         """
         # GIVEN: A new theme manager instance, with mocked builtins.open, shutil.copyfile,
         #        theme, check_directory_exists and thememanager-attributes.
-        with patch('builtins.open') as mocked_open, \
-                patch('openlp.core.ui.thememanager.shutil.copyfile') as mocked_copyfile, \
-                patch('openlp.core.ui.thememanager.check_directory_exists') as mocked_check_directory_exists:
-            mocked_open.return_value = MagicMock()
+        with patch('openlp.core.ui.thememanager.copyfile') as mocked_copyfile, \
+                patch('openlp.core.ui.thememanager.check_directory_exists'):
             theme_manager = ThemeManager(None)
             theme_manager.old_background_image = None
             theme_manager.generate_and_save_image = MagicMock()
-            theme_manager.path = ''
+            theme_manager.theme_path = MagicMock()
             mocked_theme = MagicMock()
             mocked_theme.theme_name = 'themename'
             mocked_theme.filename = "filename"
-            # mocked_theme.extract_formatted_xml = MagicMock()
-            # mocked_theme.extract_formatted_xml.return_value = 'fake_theme_xml'.encode()
 
             # WHEN: Calling _write_theme with path to different images
-            file_name1 = os.path.join(TEST_RESOURCES_PATH, 'church.jpg')
-            file_name2 = os.path.join(TEST_RESOURCES_PATH, 'church2.jpg')
+            file_name1 = Path(TEST_RESOURCES_PATH, 'church.jpg')
+            file_name2 = Path(TEST_RESOURCES_PATH, 'church2.jpg')
             theme_manager._write_theme(mocked_theme, file_name1, file_name2)
 
             # THEN: The mocked_copyfile should not have been called
-            self.assertTrue(mocked_copyfile.called, 'shutil.copyfile should be called')
+            self.assertTrue(mocked_copyfile.called, 'copyfile should be called')
 
     def test_write_theme_special_char_name(self):
         """
@@ -146,7 +139,7 @@
         theme_manager = ThemeManager(None)
         theme_manager.old_background_image = None
         theme_manager.generate_and_save_image = MagicMock()
-        theme_manager.path = self.temp_folder
+        theme_manager.theme_path = Path(self.temp_folder)
         mocked_theme = MagicMock()
         mocked_theme.theme_name = 'theme 愛 name'
         mocked_theme.export_theme.return_value = "{}"
@@ -208,17 +201,17 @@
             theme_manager = ThemeManager(None)
             theme_manager._create_theme_from_xml = MagicMock()
             theme_manager.generate_and_save_image = MagicMock()
-            theme_manager.path = ''
-            folder = mkdtemp()
+            theme_manager.theme_path = None
+            folder = Path(mkdtemp())
             theme_file = os.path.join(TEST_RESOURCES_PATH, 'themes', 'Moss_on_tree.otz')
 
             # WHEN: We try to unzip it
             theme_manager.unzip_theme(theme_file, folder)
 
             # THEN: Files should be unpacked
-            self.assertTrue(os.path.exists(os.path.join(folder, 'Moss on tree', 'Moss on tree.xml')))
+            self.assertTrue((folder / 'Moss on tree' / 'Moss on tree.xml').exists())
             self.assertEqual(mocked_critical_error_message_box.call_count, 0, 'No errors should have happened')
-            shutil.rmtree(folder)
+            shutil.rmtree(str(folder))
 
     def test_unzip_theme_invalid_version(self):
         """

=== renamed file 'tests/functional/openlp_core_ui_lib/test_color_button.py' => 'tests/functional/openlp_core_ui_lib/test_colorbutton.py'
=== modified file 'tests/functional/openlp_core_ui_lib/test_listpreviewwidget.py'
--- tests/functional/openlp_core_ui_lib/test_listpreviewwidget.py	2017-04-24 05:17:55 +0000
+++ tests/functional/openlp_core_ui_lib/test_listpreviewwidget.py	2017-06-19 07:33:18 +0000
@@ -22,14 +22,14 @@
 """
 Package to test the openlp.core.ui.lib.listpreviewwidget package.
 """
+from pathlib2 import Path
 from unittest import TestCase
 from unittest.mock import MagicMock, patch, call
 
 from PyQt5 import QtGui
 
-from openlp.core.common import Settings
 from openlp.core.ui.lib.listpreviewwidget import ListPreviewWidget
-from openlp.core.lib import ImageSource, ServiceItem
+from openlp.core.lib import ImageSource
 
 
 class TestListPreviewWidget(TestCase):
@@ -116,8 +116,8 @@
 
         # THEN: The ImageManager should be called in the appriopriate manner for each service item.
         self.assertEquals(mocked_image_manager.get_image.call_count, 4, 'Should be called once for each slide')
-        calls = [call('TEST1', ImageSource.ImagePlugin), call('TEST2', ImageSource.ImagePlugin),
-                 call('TEST3', ImageSource.CommandPlugins), call('TEST4', ImageSource.CommandPlugins)]
+        calls = [call(Path('TEST1'), ImageSource.ImagePlugin), call(Path('TEST2'), ImageSource.ImagePlugin),
+                 call(Path('TEST3'), ImageSource.CommandPlugins), call(Path('TEST4'), ImageSource.CommandPlugins)]
         mocked_image_manager.get_image.assert_has_calls(calls)
 
     @patch(u'openlp.core.ui.lib.listpreviewwidget.ListPreviewWidget.resizeRowsToContents')

=== 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-06-19 07:33:18 +0000
@@ -22,9 +22,11 @@
 """
 This module contains tests for the openlp.core.ui.lib.pathedit module
 """
+import os
+from pathlib2 import Path
 from unittest import TestCase
 
-from PyQt5 import QtWidgets
+from patches.pyqt5patches import PQFileDialog
 
 from openlp.core.ui.lib import PathEdit, PathType
 from unittest.mock import MagicMock, PropertyMock, patch
@@ -43,11 +45,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 +59,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 +127,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 +149,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 +191,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 +213,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 +229,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 +238,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 +246,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 +260,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 +274,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 +289,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'))

=== modified file 'tests/functional/openlp_plugins/bibles/test_csvimport.py'
--- tests/functional/openlp_plugins/bibles/test_csvimport.py	2017-05-08 19:04:14 +0000
+++ tests/functional/openlp_plugins/bibles/test_csvimport.py	2017-06-19 07:33:18 +0000
@@ -26,6 +26,7 @@
 import json
 import os
 from collections import namedtuple
+from pathlib2 import Path
 from unittest import TestCase
 from unittest.mock import ANY, MagicMock, PropertyMock, call, patch
 
@@ -59,12 +60,13 @@
         mocked_manager = MagicMock()
 
         # WHEN: An importer object is created
-        importer = CSVBible(mocked_manager, path='.', name='.', booksfile='books.csv', versefile='verse.csv')
+        importer = \
+            CSVBible(mocked_manager, path='.', name='.', booksfile=Path('books.csv'), versefile=Path('verse.csv'))
 
         # THEN: The importer should be an instance of BibleImport
         self.assertIsInstance(importer, BibleImport)
-        self.assertEqual(importer.books_file, 'books.csv')
-        self.assertEqual(importer.verses_file, 'verse.csv')
+        self.assertEqual(importer.books_path, Path('books.csv'))
+        self.assertEqual(importer.verses_path, Path('verse.csv'))
 
     def book_namedtuple_test(self):
         """
@@ -134,17 +136,17 @@
 
         with patch('openlp.plugins.bibles.lib.importers.csvbible.get_file_encoding',
                    return_value={'encoding': 'utf-8', 'confidence': 0.99}),\
-                patch('openlp.plugins.bibles.lib.importers.csvbible.open', create=True) as mocked_open,\
+                patch('openlp.plugins.bibles.lib.importers.csvbible.Path.open', create=True) as mocked_open,\
                 patch('openlp.plugins.bibles.lib.importers.csvbible.csv.reader',
                       return_value=iter(test_data)) as mocked_reader:
 
             # WHEN: Calling the CSVBible parse_csv_file method with a file name and TestTuple
-            result = CSVBible.parse_csv_file('file.csv', TestTuple)
+            result = CSVBible.parse_csv_file(Path('file.csv'), TestTuple)
 
             # THEN: A list of TestTuple instances with the parsed data should be returned
             self.assertEqual(result, [TestTuple('1', 'Line 1', 'Data 1'), TestTuple('2', 'Line 2', 'Data 2'),
                                       TestTuple('3', 'Line 3', 'Data 3')])
-            mocked_open.assert_called_once_with('file.csv', 'r', encoding='utf-8', newline='')
+            mocked_open.assert_called_once_with('r', encoding='utf-8', newline='')
             mocked_reader.assert_called_once_with(ANY, delimiter=',', quotechar='"')
 
     def parse_csv_file_oserror_test(self):
@@ -154,12 +156,12 @@
         # GIVEN: Mocked a mocked open object which raises an OSError
         with patch('openlp.plugins.bibles.lib.importers.csvbible.get_file_encoding',
                    return_value={'encoding': 'utf-8', 'confidence': 0.99}),\
-                patch('openlp.plugins.bibles.lib.importers.csvbible.open', side_effect=OSError, create=True):
+                patch('openlp.plugins.bibles.lib.importers.csvbible.Path.open', side_effect=OSError, create=True):
 
             # WHEN: Calling CSVBible.parse_csv_file
             # THEN: A ValidationError should be raised
             with self.assertRaises(ValidationError) as context:
-                CSVBible.parse_csv_file('file.csv', None)
+                CSVBible.parse_csv_file(Path('file.csv'), None)
             self.assertEqual(context.exception.msg, 'Parsing "file.csv" failed')
 
     def parse_csv_file_csverror_test(self):
@@ -169,13 +171,13 @@
         # GIVEN: Mocked a csv.reader which raises an csv.Error
         with patch('openlp.plugins.bibles.lib.importers.csvbible.get_file_encoding',
                    return_value={'encoding': 'utf-8', 'confidence': 0.99}),\
-                patch('openlp.plugins.bibles.lib.importers.csvbible.open', create=True),\
+                patch('openlp.plugins.bibles.lib.importers.csvbible.Path.open', create=True),\
                 patch('openlp.plugins.bibles.lib.importers.csvbible.csv.reader', side_effect=csv.Error):
 
             # WHEN: Calling CSVBible.parse_csv_file
             # THEN: A ValidationError should be raised
             with self.assertRaises(ValidationError) as context:
-                CSVBible.parse_csv_file('file.csv', None)
+                CSVBible.parse_csv_file(Path('file.csv'), None)
             self.assertEqual(context.exception.msg, 'Parsing "file.csv" failed')
 
     def process_books_stopped_import_test(self):
@@ -312,7 +314,8 @@
 
             # THEN: parse_csv_file should be called twice,
             # and True should be returned.
-            self.assertEqual(importer.parse_csv_file.mock_calls, [call('books.csv', Book), call('verses.csv', Verse)])
+            self.assertEqual(importer.parse_csv_file.mock_calls,
+                             [call(Path('books.csv'), Book), call(Path('verses.csv'), Verse)])
             importer.process_books.assert_called_once_with(['Book 1'])
             importer.process_verses.assert_called_once_with(['Verse 1'], ['Book 1'])
             self.assertTrue(result)

=== modified file 'tests/functional/openlp_plugins/bibles/test_manager.py'
--- tests/functional/openlp_plugins/bibles/test_manager.py	2016-12-31 11:01:36 +0000
+++ tests/functional/openlp_plugins/bibles/test_manager.py	2017-06-19 07:33:18 +0000
@@ -22,6 +22,7 @@
 """
 This module contains tests for the manager submodule of the Bibles plugin.
 """
+from pathlib2 import Path
 from unittest import TestCase
 from unittest.mock import MagicMock, patch
 
@@ -66,4 +67,4 @@
             self.assertTrue(result)
             mocked_close_all.assert_called_once_with()
             self.assertIsNone(mocked_bible.session)
-            mocked_delete_file.assert_called_once_with('bibles/KJV.sqlite')
+            mocked_delete_file.assert_called_once_with(Path('bibles', 'KJV.sqlite'))

=== modified file 'tests/functional/openlp_plugins/images/test_lib.py'
--- tests/functional/openlp_plugins/images/test_lib.py	2017-05-08 19:04:14 +0000
+++ tests/functional/openlp_plugins/images/test_lib.py	2017-06-19 07:33:18 +0000
@@ -22,6 +22,7 @@
 """
 This module contains tests for the lib submodule of the Images plugin.
 """
+from pathlib2 import Path
 from unittest import TestCase
 from unittest.mock import ANY, MagicMock, patch
 
@@ -57,7 +58,7 @@
         Test that the validate_and_load_test() method when called without a group
         """
         # GIVEN: A list of files
-        file_list = ['/path1/image1.jpg', '/path2/image2.jpg']
+        file_list = [Path('path1', 'image1.jpg'), Path('path2', 'image2.jpg')]
 
         # WHEN: Calling validate_and_load with the list of files
         self.media_item.validate_and_load(file_list)
@@ -65,7 +66,7 @@
         # 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().set_path_value.assert_called_once_with(ANY, Path('path1'))
 
     @patch('openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_list')
     @patch('openlp.plugins.images.lib.mediaitem.Settings')
@@ -74,7 +75,7 @@
         Test that the validate_and_load_test() method when called with a group
         """
         # GIVEN: A list of files
-        file_list = ['/path1/image1.jpg', '/path2/image2.jpg']
+        file_list = [Path('path1', 'image1.jpg'), Path('path2', 'image2.jpg')]
 
         # WHEN: Calling validate_and_load with the list of files and a group
         self.media_item.validate_and_load(file_list, 'group')
@@ -82,7 +83,7 @@
         # 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().set_path_value.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):
@@ -106,7 +107,7 @@
         Test that the save_new_images_list() calls load_full_list() when reload_list is set to True
         """
         # GIVEN: A list with 1 image and a mocked out manager
-        image_list = ['test_image.jpg']
+        image_list = [Path('test_image.jpg')]
         ImageFilenames.filename = ''
         self.media_item.manager = MagicMock()
 
@@ -125,7 +126,7 @@
         Test that the save_new_images_list() doesn't call load_full_list() when reload_list is set to False
         """
         # GIVEN: A list with 1 image and a mocked out manager
-        image_list = ['test_image.jpg']
+        image_list = [Path('test_image.jpg')]
         self.media_item.manager = MagicMock()
 
         # WHEN: We run save_new_images_list with reload_list=False
@@ -140,7 +141,7 @@
         Test that the save_new_images_list() saves all images in the list
         """
         # GIVEN: A list with 3 images
-        image_list = ['test_image_1.jpg', 'test_image_2.jpg', 'test_image_3.jpg']
+        image_list = [Path('test_image_1.jpg'), Path('test_image_2.jpg'), Path('test_image_3.jpg')]
         self.media_item.manager = MagicMock()
 
         # WHEN: We run save_new_images_list with the list of 3 images
@@ -156,7 +157,7 @@
         Test that the save_new_images_list() ignores everything in the provided list except strings
         """
         # GIVEN: A list with images and objects
-        image_list = ['test_image_1.jpg', None, True, ImageFilenames(), 'test_image_2.jpg']
+        image_list = [Path('test_image_1.jpg'), None, True, ImageFilenames(), Path('test_image_2.jpg')]
         self.media_item.manager = MagicMock()
 
         # WHEN: We run save_new_images_list with the list of images and objects
@@ -190,7 +191,7 @@
         ImageGroups.parent_id = 1
         self.media_item.manager = MagicMock()
         self.media_item.manager.get_all_objects.side_effect = self._recursively_delete_group_side_effect
-        self.media_item.service_path = ''
+        self.media_item.service_path = Path()
         test_group = ImageGroups()
         test_group.id = 1
 
@@ -244,7 +245,7 @@
         test_image.group_id = 1
         test_image.filename = 'imagefile.png'
         self.media_item.manager = MagicMock()
-        self.media_item.service_path = ''
+        self.media_item.service_path = Path()
         self.media_item.list_view = MagicMock()
         mocked_row_item = MagicMock()
         mocked_row_item.data.return_value = test_image

=== modified file 'tests/functional/openlp_plugins/media/test_mediaplugin.py'
--- tests/functional/openlp_plugins/media/test_mediaplugin.py	2017-04-24 05:17:55 +0000
+++ tests/functional/openlp_plugins/media/test_mediaplugin.py	2017-06-19 07:33:18 +0000
@@ -38,22 +38,18 @@
     def setUp(self):
         Registry.create()
 
-    @patch(u'openlp.plugins.media.mediaplugin.Plugin.initialise')
-    @patch(u'openlp.plugins.media.mediaplugin.Settings')
-    def test_initialise(self, _mocked_settings, mocked_initialise):
+    @patch('openlp.plugins.media.mediaplugin.Plugin.initialise')
+    def test_initialise(self, mocked_initialise):
         """
         Test that the initialise() method overwrites the built-in one, but still calls it
         """
-        # GIVEN: A media plugin instance and a mocked settings object
+        # GIVEN: A media plugin instance
         media_plugin = MediaPlugin()
-        mocked_settings = MagicMock()
-        mocked_settings.get_files_from_config.return_value = True  # Not the real value, just need something "true-ish"
-        _mocked_settings.return_value = mocked_settings
 
         # WHEN: initialise() is called
         media_plugin.initialise()
 
-        # THEN: The settings should be upgraded and the base initialise() method should be called
+        # THEN: The the base initialise() method should be called
         mocked_initialise.assert_called_with()
 
     def test_about_text(self):

=== modified file 'tests/functional/openlp_plugins/presentations/test_impresscontroller.py'
--- tests/functional/openlp_plugins/presentations/test_impresscontroller.py	2017-04-24 05:17:55 +0000
+++ tests/functional/openlp_plugins/presentations/test_impresscontroller.py	2017-06-19 07:33:18 +0000
@@ -23,9 +23,9 @@
 Functional tests to test the Impress class and related methods.
 """
 from unittest import TestCase
-from unittest.mock import patch, MagicMock
-import os
+from unittest.mock import  MagicMock
 import shutil
+from pathlib2 import Path
 from tempfile import mkdtemp
 
 from openlp.core.common import Settings
@@ -82,7 +82,7 @@
         mocked_plugin = MagicMock()
         mocked_plugin.settings_section = 'presentations'
         Settings().extend_default_settings(__default_settings__)
-        self.file_name = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'test.pptx')
+        self.file_name = Path(TEST_RESOURCES_PATH, 'presentations', 'test.pptx')
         self.ppc = ImpressController(mocked_plugin)
         self.doc = ImpressDocument(self.ppc, self.file_name)
 

=== modified file 'tests/functional/openlp_plugins/presentations/test_mediaitem.py'
--- tests/functional/openlp_plugins/presentations/test_mediaitem.py	2017-04-24 05:17:55 +0000
+++ tests/functional/openlp_plugins/presentations/test_mediaitem.py	2017-06-19 07:33:18 +0000
@@ -22,6 +22,7 @@
 """
 This module contains tests for the lib submodule of the Presentations plugin.
 """
+from pathlib2 import Path
 from unittest import TestCase
 from unittest.mock import patch, MagicMock, call
 
@@ -98,7 +99,7 @@
         self.media_item.controllers = {
             'Mocked': mocked_controller
         }
-        presentation_file = 'file.tmp'
+        presentation_file = Path('file.tmp')
         with patch('openlp.plugins.presentations.lib.mediaitem.os.path.getmtime') as mocked_getmtime, \
                 patch('openlp.plugins.presentations.lib.mediaitem.os.path.exists') as mocked_exists:
             mocked_getmtime.side_effect = [100, 200]
@@ -123,7 +124,7 @@
         self.media_item.controllers = {
             'Mocked': mocked_controller
         }
-        presentation_file = 'file.tmp'
+        presentation_file = Path('file.tmp')
         with patch('openlp.plugins.presentations.lib.mediaitem.os.path.exists') as mocked_exists:
             mocked_exists.return_value = False
 

=== modified file 'tests/functional/openlp_plugins/presentations/test_pdfcontroller.py'
--- tests/functional/openlp_plugins/presentations/test_pdfcontroller.py	2017-04-24 05:17:55 +0000
+++ tests/functional/openlp_plugins/presentations/test_pdfcontroller.py	2017-06-19 07:33:18 +0000
@@ -24,6 +24,7 @@
 """
 import os
 import shutil
+from pathlib2 import Path
 from tempfile import mkdtemp
 from unittest import TestCase, SkipTest
 from unittest.mock import MagicMock, patch
@@ -66,8 +67,8 @@
         self.desktop.screenGeometry.return_value = SCREEN['size']
         self.screens = ScreenList.create(self.desktop)
         Settings().extend_default_settings(__default_settings__)
-        self.temp_folder = mkdtemp()
-        self.thumbnail_folder = mkdtemp()
+        self.temp_folder = Path(mkdtemp())
+        self.thumbnail_folder = Path(mkdtemp())
         self.mock_plugin = MagicMock()
         self.mock_plugin.settings_section = self.temp_folder
 
@@ -77,8 +78,8 @@
         """
         del self.screens
         self.destroy_settings()
-        shutil.rmtree(self.thumbnail_folder)
-        shutil.rmtree(self.temp_folder)
+        shutil.rmtree(str(self.thumbnail_folder))
+        shutil.rmtree(str(self.temp_folder))
 
     def test_constructor(self):
         """
@@ -98,7 +99,7 @@
         Test loading of a Pdf using the PdfController
         """
         # GIVEN: A Pdf-file
-        test_file = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'pdf_test1.pdf')
+        test_file = Path(TEST_RESOURCES_PATH, 'presentations', 'pdf_test1.pdf')
 
         # WHEN: The Pdf is loaded
         controller = PdfController(plugin=self.mock_plugin)
@@ -118,7 +119,7 @@
         Test loading of a Pdf and check size of generate pictures
         """
         # GIVEN: A Pdf-file
-        test_file = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'pdf_test1.pdf')
+        test_file = Path(TEST_RESOURCES_PATH, 'presentations', 'pdf_test1.pdf')
 
         # WHEN: The Pdf is loaded
         controller = PdfController(plugin=self.mock_plugin)
@@ -131,7 +132,7 @@
 
         # THEN: The load should succeed and pictures should be created and have been scales to fit the screen
         self.assertTrue(loaded, 'The loading of the PDF should succeed.')
-        image = QtGui.QImage(os.path.join(self.temp_folder, 'pdf_test1.pdf', 'mainslide001.png'))
+        image = QtGui.QImage(os.path.join(str(self.temp_folder), 'pdf_test1.pdf', 'mainslide001.png'))
         # Based on the converter used the resolution will differ a bit
         if controller.gsbin:
             self.assertEqual(760, image.height(), 'The height should be 760')

=== modified file 'tests/functional/openlp_plugins/presentations/test_presentationcontroller.py'
--- tests/functional/openlp_plugins/presentations/test_presentationcontroller.py	2017-05-30 18:42:35 +0000
+++ tests/functional/openlp_plugins/presentations/test_presentationcontroller.py	2017-06-19 07:33:18 +0000
@@ -24,8 +24,9 @@
 classes and related methods.
 """
 import os
+from pathlib2 import Path
 from unittest import TestCase
-from unittest.mock import MagicMock, mock_open, patch
+from unittest.mock import MagicMock, call, patch
 
 from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
 
@@ -38,7 +39,8 @@
     """
     def setUp(self):
         self.get_thumbnail_folder_patcher = \
-            patch('openlp.plugins.presentations.lib.presentationcontroller.PresentationDocument.get_thumbnail_folder')
+            patch('openlp.plugins.presentations.lib.presentationcontroller.PresentationDocument.get_thumbnail_folder',
+            return_value=Path())
         self.get_thumbnail_folder_patcher.start()
         mocked_plugin = MagicMock()
         mocked_plugin.settings_section = 'presentations'
@@ -65,23 +67,18 @@
         Test PresentationDocument.save_titles_and_notes method with two valid lists
         """
         # GIVEN: two lists of length==2 and a mocked open and get_thumbnail_folder
-        mocked_open = mock_open()
-        with patch('builtins.open', mocked_open), patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder:
+        with patch('openlp.plugins.presentations.lib.presentationcontroller.Path.write_text') as mocked_write_text, \
+                patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder:
             titles = ['uno', 'dos']
             notes = ['one', 'two']
 
             # WHEN: calling save_titles_and_notes
-            mocked_get_thumbnail_folder.return_value = 'test'
+            mocked_get_thumbnail_folder.return_value = Path('test')
             self.document.save_titles_and_notes(titles, notes)
 
             # THEN: the last call to open should have been for slideNotes2.txt
-            mocked_open.assert_any_call(os.path.join('test', 'titles.txt'), mode='wt', encoding='utf-8')
-            mocked_open.assert_any_call(os.path.join('test', 'slideNotes1.txt'), mode='wt', encoding='utf-8')
-            mocked_open.assert_any_call(os.path.join('test', 'slideNotes2.txt'), mode='wt', encoding='utf-8')
-            self.assertEqual(mocked_open.call_count, 3, 'There should be exactly three files opened')
-            mocked_open().writelines.assert_called_once_with(['uno', 'dos'])
-            mocked_open().write.assert_any_call('one')
-            mocked_open().write.assert_any_call('two')
+            self.assertEqual(mocked_write_text.call_count, 3, 'There should be exactly three files written')
+            mocked_write_text.assert_has_calls([call('uno\ndos'), call('one'), call('two')])
 
     def test_save_titles_and_notes_with_None(self):
         """
@@ -105,10 +102,11 @@
         """
         # GIVEN: A mocked open, get_thumbnail_folder and exists
 
-        with patch('builtins.open', mock_open(read_data='uno\ndos\n')) as mocked_open, \
+        with patch('openlp.plugins.presentations.lib.presentationcontroller.Path.read_text',
+                   return_value='uno\ndos\n') as mocked_read_text, \
                 patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder, \
-                patch('openlp.plugins.presentations.lib.presentationcontroller.os.path.exists') as mocked_exists:
-            mocked_get_thumbnail_folder.return_value = 'test'
+                patch('openlp.plugins.presentations.lib.presentationcontroller.Path.exists') as mocked_exists:
+            mocked_get_thumbnail_folder.return_value = Path('test')
             mocked_exists.return_value = True
 
             # WHEN: calling get_titles_and_notes
@@ -119,45 +117,36 @@
             self.assertEqual(len(result_titles), 2, 'There should be two items in the titles')
             self.assertIs(type(result_notes), list, 'result_notes should be of type list')
             self.assertEqual(len(result_notes), 2, 'There should be two items in the notes')
-            self.assertEqual(mocked_open.call_count, 3, 'Three files should be opened')
-            mocked_open.assert_any_call(os.path.join('test', 'titles.txt'), encoding='utf-8')
-            mocked_open.assert_any_call(os.path.join('test', 'slideNotes1.txt'), encoding='utf-8')
-            mocked_open.assert_any_call(os.path.join('test', 'slideNotes2.txt'), encoding='utf-8')
-            self.assertEqual(mocked_exists.call_count, 3, 'Three files should have been checked')
+            self.assertEqual(mocked_read_text.call_count, 3, 'Three files should be read')
 
     def test_get_titles_and_notes_with_file_not_found(self):
         """
         Test PresentationDocument.get_titles_and_notes method with file not found
         """
         # GIVEN: A mocked open, get_thumbnail_folder and exists
-        with patch('builtins.open') as mocked_open, \
-                patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder, \
-                patch('openlp.plugins.presentations.lib.presentationcontroller.os.path.exists') as mocked_exists:
-            mocked_get_thumbnail_folder.return_value = 'test'
-            mocked_exists.return_value = False
+        with patch('openlp.plugins.presentations.lib.presentationcontroller.Path.read_text') as mocked_read_text, \
+                patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder:
+            mocked_read_text.side_effect = FileNotFoundError()
+            mocked_get_thumbnail_folder.return_value = Path('test')
 
             # WHEN: calling get_titles_and_notes
             result_titles, result_notes = self.document.get_titles_and_notes()
 
             # THEN: it should return two empty lists
-            self.assertIs(type(result_titles), list, 'result_titles should be of type list')
+            self.assertIsInstance(result_titles, list, 'result_titles should be of type list')
             self.assertEqual(len(result_titles), 0, 'there be no titles')
-            self.assertIs(type(result_notes), list, 'result_notes should be a list')
+            self.assertIsInstance(result_notes, list, 'result_notes should be a list')
             self.assertEqual(len(result_notes), 0, 'but the list should be empty')
-            self.assertEqual(mocked_open.call_count, 0, 'No calls to open files')
-            self.assertEqual(mocked_exists.call_count, 1, 'There should be one call to file exists')
 
     def test_get_titles_and_notes_with_file_error(self):
         """
         Test PresentationDocument.get_titles_and_notes method with file errors
         """
         # GIVEN: A mocked open, get_thumbnail_folder and exists
-        with patch('builtins.open') as mocked_open, \
-                patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder, \
-                patch('openlp.plugins.presentations.lib.presentationcontroller.os.path.exists') as mocked_exists:
-            mocked_get_thumbnail_folder.return_value = 'test'
-            mocked_exists.return_value = True
-            mocked_open.side_effect = IOError()
+        with patch('openlp.plugins.presentations.lib.presentationcontroller.Path.read_text') as mocked_read_text, \
+                patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder:
+            mocked_read_text.side_effect = IOError()
+            mocked_get_thumbnail_folder.return_value = Path('test')
 
             # WHEN: calling get_titles_and_notes
             result_titles, result_notes = self.document.get_titles_and_notes()
@@ -189,7 +178,7 @@
 
         self.mock_controller = MagicMock()
 
-        self.mock_get_thumbnail_folder.return_value = 'returned/path/'
+        self.mock_get_thumbnail_folder.return_value = Path('returned/path/')
 
     def tearDown(self):
         """
@@ -225,7 +214,7 @@
         PresentationDocument(self.mock_controller, 'Name')
 
         # THEN: check_directory_exists should have been called with 'returned/path/'
-        self.mock_check_directory_exists.assert_called_once_with('returned/path/')
+        self.mock_check_directory_exists.assert_called_once_with(Path('returned', 'path/'))
 
         self._setup_patcher.start()
 
@@ -242,20 +231,3 @@
 
         # THEN: load_presentation should return false
         self.assertFalse(result, "PresentationDocument.load_presentation should return false.")
-
-    def test_get_file_name(self):
-        """
-        Test the PresentationDocument.get_file_name method.
-        """
-
-        # GIVEN: A mocked os.path.split which returns a list, an instance of PresentationDocument and
-        #       arbitary file_path.
-        self.mock_os.path.split.return_value = ['directory', 'file.ext']
-        instance = PresentationDocument(self.mock_controller, 'Name')
-        instance.file_path = 'filepath'
-
-        # WHEN: Calling get_file_name
-        result = instance.get_file_name()
-
-        # THEN: get_file_name should return 'file.ext'
-        self.assertEqual(result, 'file.ext')

=== modified file 'tests/functional/openlp_plugins/remotes/test_router.py'
--- tests/functional/openlp_plugins/remotes/test_router.py	2017-03-23 05:07:16 +0000
+++ tests/functional/openlp_plugins/remotes/test_router.py	2017-06-19 07:33:18 +0000
@@ -24,6 +24,7 @@
 """
 import os
 import urllib.request
+from pathlib2 import Path
 from unittest import TestCase
 from unittest.mock import MagicMock, patch, mock_open
 
@@ -53,6 +54,7 @@
         Create the UI
         """
         Registry.create()
+        Registry().register('main_window', MagicMock())
         self.setup_application()
         self.build_settings()
         Settings().extend_default_settings(__default_settings__)
@@ -302,7 +304,8 @@
         height = 90
         with patch('openlp.core.lib.os.path.exists') as mocked_exists, \
                 patch('builtins.open', mock_open(read_data='123')), \
-                patch('openlp.plugins.remotes.lib.httprouter.AppLocation') as mocked_location, \
+                patch('openlp.plugins.remotes.lib.httprouter.AppLocation.get_section_data_path', return_value=Path())\
+                        as mocked_location, \
                 patch('openlp.plugins.remotes.lib.httprouter.image_to_byte') as mocked_image_to_byte:
             mocked_exists.return_value = True
             mocked_image_to_byte.return_value = '123'
@@ -320,8 +323,7 @@
             mocked_image_manager.add_image.assert_any_call(os.path.normpath(os.path.join('thumbnails', 'another test',
                                                                                          'slide1.png')),
                                                            'slide1.png', None, width, height)
-            mocked_image_manager.get_image.assert_any_call(os.path.normpath(os.path.join('thumbnails', 'another test',
-                                                                                         'slide1.png')),
+            mocked_image_manager.get_image.assert_any_call(Path('thumbnails', 'another test', 'slide1.png'),
                                                            'slide1.png', width, height)
 
     def test_remote_next(self):

=== modified file 'tests/functional/openlp_plugins/songs/test_chordproimport.py'
--- tests/functional/openlp_plugins/songs/test_chordproimport.py	2017-05-11 20:24:20 +0000
+++ tests/functional/openlp_plugins/songs/test_chordproimport.py	2017-06-19 07:33:18 +0000
@@ -23,6 +23,7 @@
 This module contains tests for the OpenSong song importer.
 """
 import os
+from pathlib2 import Path
 
 from tests.helpers.songfileimport import SongImportTestHelper
 from unittest.mock import patch, MagicMock
@@ -48,5 +49,5 @@
         mocked_returned_settings.value.side_effect = lambda value: True if value == 'songs/enable chords' else False
         mocked_settings.return_value = mocked_returned_settings
         # Do the test import
-        self.file_import([os.path.join(TEST_PATH, 'swing-low.chordpro')],
+        self.file_import([Path(TEST_PATH, 'swing-low.chordpro')],
                          self.load_external_result_data(os.path.join(TEST_PATH, 'swing-low.json')))

=== modified file 'tests/functional/openlp_plugins/songs/test_easyslidesimport.py'
--- tests/functional/openlp_plugins/songs/test_easyslidesimport.py	2017-01-12 21:31:01 +0000
+++ tests/functional/openlp_plugins/songs/test_easyslidesimport.py	2017-06-19 07:33:18 +0000
@@ -23,6 +23,7 @@
 """
 
 import os
+from pathlib2 import Path
 
 from tests.helpers.songfileimport import SongImportTestHelper
 
@@ -41,7 +42,7 @@
         """
         Test that loading an EasySlides file works correctly on various files
         """
-        self.file_import(os.path.join(TEST_PATH, 'amazing-grace.xml'),
+        self.file_import(Path(TEST_PATH, 'amazing-grace.xml'),
                          self.load_external_result_data(os.path.join(TEST_PATH, 'Amazing Grace.json')))
-        self.file_import(os.path.join(TEST_PATH, 'Export_2017-01-12_BB.xml'),
+        self.file_import(Path(TEST_PATH, 'Export_2017-01-12_BB.xml'),
                          self.load_external_result_data(os.path.join(TEST_PATH, 'Export_2017-01-12_BB.json')))

=== modified file 'tests/functional/openlp_plugins/songs/test_ewimport.py'
--- tests/functional/openlp_plugins/songs/test_ewimport.py	2017-05-22 19:07:07 +0000
+++ tests/functional/openlp_plugins/songs/test_ewimport.py	2017-06-19 07:33:18 +0000
@@ -97,7 +97,7 @@
     _title_assignment_list = []
 
     def __init__(self, manager):
-        EasyWorshipSongImport.__init__(self, manager, filenames=[])
+        EasyWorshipSongImport.__init__(self, manager, file_paths=[])
 
     @property
     def title(self):
@@ -180,7 +180,7 @@
             mocked_manager = MagicMock()
 
             # WHEN: An importer object is created
-            importer = EasyWorshipSongImport(mocked_manager, filenames=[])
+            importer = EasyWorshipSongImport(mocked_manager, file_paths=[])
 
             # THEN: The importer object should not be None
             self.assertIsNotNone(importer, 'Import should not be none')
@@ -192,7 +192,7 @@
         # GIVEN: A mocked out SongImport class, a mocked out "manager" and a list of field descriptions.
         with patch('openlp.plugins.songs.lib.importers.easyworship.SongImport'):
             mocked_manager = MagicMock()
-            importer = EasyWorshipSongImport(mocked_manager, filenames=[])
+            importer = EasyWorshipSongImport(mocked_manager, file_paths=[])
             importer.field_descriptions = TEST_FIELD_DESCS
 
             # WHEN: Called with a field name that exists
@@ -210,7 +210,7 @@
         # GIVEN: A mocked out SongImport class, a mocked out "manager" and a list of field descriptions
         with patch('openlp.plugins.songs.lib.importers.easyworship.SongImport'):
             mocked_manager = MagicMock()
-            importer = EasyWorshipSongImport(mocked_manager, filenames=[])
+            importer = EasyWorshipSongImport(mocked_manager, file_paths=[])
             importer.field_descriptions = TEST_FIELD_DESCS
 
             # WHEN: Called with a field name that does not exist
@@ -229,7 +229,7 @@
         with patch('openlp.plugins.songs.lib.importers.easyworship.SongImport'), \
                 patch('openlp.plugins.songs.lib.importers.easyworship.struct') as mocked_struct:
             mocked_manager = MagicMock()
-            importer = EasyWorshipSongImport(mocked_manager, filenames=[])
+            importer = EasyWorshipSongImport(mocked_manager, file_paths=[])
 
             # WHEN: db_set_record_struct is called with a list of field descriptions
             return_value = importer.db_set_record_struct(TEST_FIELD_DESCS)
@@ -246,7 +246,7 @@
         # GIVEN: A mocked out SongImport class, a mocked out "manager", an encoding and some test data and known results
         with patch('openlp.plugins.songs.lib.importers.easyworship.SongImport'):
             mocked_manager = MagicMock()
-            importer = EasyWorshipSongImport(mocked_manager, filenames=[])
+            importer = EasyWorshipSongImport(mocked_manager, file_paths=[])
             importer.encoding = TEST_DATA_ENCODING
             importer.fields = TEST_FIELDS
             importer.field_descriptions = TEST_FIELD_DESCS
@@ -270,7 +270,7 @@
             with patch('openlp.plugins.songs.lib.importers.easyworship.SongImport'):
                 mocked_manager = MagicMock()
                 mocked_memo_file = MagicMock()
-                importer = EasyWorshipSongImport(mocked_manager, filenames=[])
+                importer = EasyWorshipSongImport(mocked_manager, file_paths=[])
                 importer.memo_file = mocked_memo_file
                 importer.encoding = TEST_DATA_ENCODING
 
@@ -294,44 +294,26 @@
                     else:
                         mocked_memo_file.seek.assert_any_call(call[0], call[1])
 
-    def test_do_import_source(self):
-        """
-        Test the :mod:`do_import` module opens the correct files
-        """
-        # GIVEN: A mocked out SongImport class, a mocked out "manager"
-        with patch('openlp.plugins.songs.lib.importers.easyworship.SongImport'), \
-                patch('openlp.plugins.songs.lib.importers.easyworship.os.path') as mocked_os_path:
-            mocked_manager = MagicMock()
-            importer = EasyWorshipSongImport(mocked_manager, filenames=[])
-            mocked_os_path.isfile.side_effect = [True, False]
-
-            # WHEN: Supplied with an import source
-            importer.import_source = 'Songs.DB'
-
-            # THEN: do_import should return None having called os.path.isfile
-            self.assertIsNone(importer.do_import(), 'do_import should return None')
-            mocked_os_path.isfile.assert_any_call('Songs.DB')
-            mocked_os_path.isfile.assert_any_call('Songs.MB')
-
     def test_do_import_source_invalid(self):
         """
         Test the :mod:`do_import` module produces an error when Songs.MB not found.
         """
         # GIVEN: A mocked out SongImport class, a mocked out "manager"
         with patch('openlp.plugins.songs.lib.importers.easyworship.SongImport'), \
-                patch('openlp.plugins.songs.lib.importers.easyworship.os.path') as mocked_os_path:
+                patch('openlp.plugins.songs.lib.importers.easyworship.Path.is_file', side_effect=[True, False]):
             mocked_manager = MagicMock()
-            importer = EasyWorshipSongImport(mocked_manager, filenames=[])
-            importer.log_error = MagicMock()
-            mocked_os_path.isfile.side_effect = [True, False]
-
-            # WHEN: do_import is supplied with an import source (Songs.MB missing)
-            importer.import_source = 'Songs.DB'
-            importer.do_import()
-
-            # THEN: do_import should have logged an error that the Songs.MB file could not be found.
-            importer.log_error.assert_any_call(importer.import_source, 'Could not find the "Songs.MB" file. It must be '
-                                                                       'in the same folder as the "Songs.DB" file.')
+            importer = EasyWorshipSongImport(mocked_manager, file_paths=[])
+            with patch.object(importer, 'log_error') as mocked_log_error:
+                #importer.log_error = MagicMock()
+
+                # WHEN: do_import is supplied with an import source (Songs.MB missing)
+                importer.import_source = 'Songs.DB'
+                importer.do_import()
+
+                # THEN: do_import should have logged an error that the Songs.MB file could not be found.
+                mocked_log_error.assert_any_call(importer.import_source,
+                                                 'Could not find the "Songs.MB" file. It must be in the same folder as '
+                                                 'the "Songs.DB" file.')
 
     def test_do_import_database_validity(self):
         """
@@ -339,18 +321,19 @@
         """
         # GIVEN: A mocked out SongImport class, os.path and a mocked out "manager"
         with patch('openlp.plugins.songs.lib.importers.easyworship.SongImport'), \
-                patch('openlp.plugins.songs.lib.importers.easyworship.os.path') as mocked_os_path:
+                patch('openlp.plugins.songs.lib.importers.easyworship.Path.is_file', return_value=True), \
+                patch('openlp.plugins.songs.lib.importers.easyworship.Path.stat') as mocked_stat:
+
             mocked_manager = MagicMock()
-            importer = EasyWorshipSongImport(mocked_manager, filenames=[])
-            mocked_os_path.isfile.return_value = True
+            importer = EasyWorshipSongImport(mocked_manager, file_paths=[])
             importer.import_source = 'Songs.DB'
 
             # WHEN: DB file size is less than 0x800
-            mocked_os_path.getsize.return_value = 0x7FF
+            mocked_stat.return_value.st_size = 0x7FF
 
-            # THEN: do_import should return None having called os.path.isfile
+            # THEN: do_import should return None having called Path.stat()
             self.assertIsNone(importer.do_import(), 'do_import should return None when db_size is less than 0x800')
-            mocked_os_path.getsize.assert_any_call('Songs.DB')
+            mocked_stat.assert_called_once_with()
 
     def test_do_import_memo_validty(self):
         """
@@ -358,13 +341,12 @@
         """
         # GIVEN: A mocked out SongImport class, a mocked out "manager"
         with patch('openlp.plugins.songs.lib.importers.easyworship.SongImport'), \
-            patch('openlp.plugins.songs.lib.importers.easyworship.os.path') as mocked_os_path, \
-            patch('builtins.open') as mocked_open, \
+                patch('openlp.plugins.songs.lib.importers.easyworship.Path.is_file', return_value=True), \
+                patch('openlp.plugins.songs.lib.importers.easyworship.Path.stat', **{'return_value.st_size': 0x800}), \
+                patch('openlp.plugins.songs.lib.importers.easyworship.Path.open') as mocked_open, \
                 patch('openlp.plugins.songs.lib.importers.easyworship.struct') as mocked_struct:
             mocked_manager = MagicMock()
-            importer = EasyWorshipSongImport(mocked_manager, filenames=[])
-            mocked_os_path.isfile.return_value = True
-            mocked_os_path.getsize.return_value = 0x800
+            importer = EasyWorshipSongImport(mocked_manager, file_paths=[])
             importer.import_source = 'Songs.DB'
 
             # WHEN: Unpacking first 35 bytes of Memo file
@@ -385,14 +367,14 @@
         """
         # GIVEN: A mocked out SongImport class, a mocked out "manager"
         with patch('openlp.plugins.songs.lib.importers.easyworship.SongImport'), \
-            patch('openlp.plugins.songs.lib.importers.easyworship.os.path') as mocked_os_path, \
+            patch('openlp.plugins.songs.lib.importers.easyworship.Path.is_file', return_value=True), \
+            patch('openlp.plugins.songs.lib.importers.easyworship.Path.stat', **{'return_value.st_size':0x800}), \
+            patch('openlp.plugins.songs.lib.importers.easyworship.Path.open'), \
             patch('builtins.open'), patch('openlp.plugins.songs.lib.importers.easyworship.struct') as mocked_struct, \
-                patch('openlp.plugins.songs.lib.importers.easyworship.retrieve_windows_encoding') as \
+            patch('openlp.plugins.songs.lib.importers.easyworship.retrieve_windows_encoding') as \
                 mocked_retrieve_windows_encoding:
             mocked_manager = MagicMock()
-            importer = EasyWorshipSongImport(mocked_manager, filenames=[])
-            mocked_os_path.isfile.return_value = True
-            mocked_os_path.getsize.return_value = 0x800
+            importer = EasyWorshipSongImport(mocked_manager, file_paths=[])
             importer.import_source = 'Songs.DB'
 
             # WHEN: Unpacking the code page

=== modified file 'tests/functional/openlp_plugins/songs/test_lyriximport.py'
--- tests/functional/openlp_plugins/songs/test_lyriximport.py	2017-04-24 05:17:55 +0000
+++ tests/functional/openlp_plugins/songs/test_lyriximport.py	2017-06-19 07:33:18 +0000
@@ -22,7 +22,7 @@
 This module contains tests for the LyriX song importer.
 """
 import os
-from unittest.mock import patch
+from pathlib2 import Path
 
 from tests.helpers.songfileimport import SongImportTestHelper
 
@@ -41,9 +41,9 @@
         """
         Test that loading an LyriX file works correctly on various files
         """
-        self.file_import([os.path.join(TEST_PATH, 'A06.TXT')],
+        self.file_import([Path(TEST_PATH, 'A06.TXT')],
                          self.load_external_result_data(os.path.join(TEST_PATH, 'Amazing Grace.json')))
-        self.file_import([os.path.join(TEST_PATH, 'A002.TXT')],
+        self.file_import([Path(TEST_PATH, 'A002.TXT')],
                          self.load_external_result_data(os.path.join(TEST_PATH, 'Amazing Grace2.json')))
-        self.file_import([os.path.join(TEST_PATH, 'AO05.TXT')],
+        self.file_import([Path(TEST_PATH, 'AO05.TXT')],
                          self.load_external_result_data(os.path.join(TEST_PATH, 'in die regterhand.json')))

=== modified file 'tests/functional/openlp_plugins/songs/test_mediashout.py'
--- tests/functional/openlp_plugins/songs/test_mediashout.py	2017-04-24 05:17:55 +0000
+++ tests/functional/openlp_plugins/songs/test_mediashout.py	2017-06-19 07:

Follow ups