← Back to team overview

openlp-core team mailing list archive

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

 

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

Requested reviews:
  Tim Bentley (trb143)

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

More pathlib changes, focused mainly on the presentation plugin

lp:~phill-ridout/openlp/pathlib5 (revision 2774)
[SUCCESS] https://ci.openlp.io/job/Branch-01-Pull/2190/
[SUCCESS] https://ci.openlp.io/job/Branch-02-Functional-Tests/2093/
[SUCCESS] https://ci.openlp.io/job/Branch-03-Interface-Tests/1980/
[SUCCESS] https://ci.openlp.io/job/Branch-04a-Code_Analysis/1350/
[SUCCESS] https://ci.openlp.io/job/Branch-04b-Test_Coverage/1185/
[SUCCESS] https://ci.openlp.io/job/Branch-04c-Code_Analysis2/315/
[FAILURE] https://ci.openlp.io/job/Branch-05-AppVeyor-Tests/157/
Stopping after failure
-- 
Your team OpenLP Core is subscribed to branch lp:openlp.
=== modified file 'openlp/core/__init__.py'
--- openlp/core/__init__.py	2017-09-03 10:18:14 +0000
+++ openlp/core/__init__.py	2017-09-18 20:44:53 +0000
@@ -29,8 +29,6 @@
 
 import argparse
 import logging
-import os
-import shutil
 import sys
 import time
 from datetime import datetime
@@ -43,6 +41,7 @@
 from openlp.core.common.path import Path
 from openlp.core.common.versionchecker import VersionThread, get_application_version
 from openlp.core.lib import ScreenList
+from openlp.core.lib.shutil import copytree
 from openlp.core.resources import qInitResources
 from openlp.core.ui import SplashScreen
 from openlp.core.ui.exceptionform import ExceptionForm
@@ -181,25 +180,20 @@
         """
         Check if the data folder path exists.
         """
-        data_folder_path = str(AppLocation.get_data_path())
-        if not os.path.exists(data_folder_path):
-            log.critical('Database was not found in: ' + 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 '
-                                                                        'previously changed from the OpenLP\'s '
-                                                                        'default location. If the data was stored on '
-                                                                        'removable device, that device needs to be '
-                                                                        'made available.\n\nYou may reset the data '
-                                                                        'location back to the default location, '
-                                                                        'or you can try to make the current location '
-                                                                        'available.\n\nDo you want to reset to the '
-                                                                        'default data location? If not, OpenLP will be '
-                                                                        'closed so you can try to fix the the problem.')
-                                                    .format(path=data_folder_path),
-                                                    QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes |
-                                                                                          QtWidgets.QMessageBox.No),
-                                                    QtWidgets.QMessageBox.No)
+        data_folder_path = AppLocation.get_data_path()
+        if not data_folder_path.exists():
+            log.critical('Database was not found in: %s', 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 previously changed from the OpenLP\'s default location. If the data was '
+                                    'stored on removable device, that device needs to be made available.\n\nYou may '
+                                    'reset the data location back to the default location, or you can try to make the '
+                                    'current location available.\n\nDo you want to reset to the default data location? '
+                                    'If not, OpenLP will be closed so you can try to fix the the problem.')
+                .format(path=data_folder_path),
+                QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No),
+                QtWidgets.QMessageBox.No)
             if status == QtWidgets.QMessageBox.No:
                 # If answer was "No", return "True", it will shutdown OpenLP in def main
                 log.info('User requested termination')
@@ -253,11 +247,11 @@
                                                                   'a backup of the old data folder?'),
                                               defaultButton=QtWidgets.QMessageBox.Yes) == QtWidgets.QMessageBox.Yes:
                 # Create copy of data folder
-                data_folder_path = str(AppLocation.get_data_path())
+                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!'))

=== modified file 'openlp/core/common/applocation.py'
--- openlp/core/common/applocation.py	2017-08-26 15:06:11 +0000
+++ openlp/core/common/applocation.py	2017-09-18 20:44:53 +0000
@@ -29,7 +29,6 @@
 from openlp.core.common import Settings, is_win, is_macosx
 from openlp.core.common.path import Path
 
-
 if not is_win() and not is_macosx():
     try:
         from xdg import BaseDirectory

=== modified file 'openlp/core/common/httputils.py'
--- openlp/core/common/httputils.py	2017-08-13 05:50:44 +0000
+++ openlp/core/common/httputils.py	2017-09-18 20:44:53 +0000
@@ -211,7 +211,7 @@
 
     :param callback: the class which needs to be updated
     :param url: URL to download
-    :param f_path: Destination file
+    :param openlp.core.common.path.Path f_path: Destination file
     :param sha256: The check sum value to be checked against the download value
     """
     block_count = 0
@@ -220,29 +220,23 @@
     log.debug("url_get_file: " + url)
     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)
-                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:
+            with f_path.open('wb') as file:
+                url_file = urllib.request.urlopen(url, timeout=CONNECTION_TIMEOUT)
+                # Download until finished or canceled.
+                while not callback.was_cancelled:
+                    data = url_file.read(block_size)
+                    if not data:
+                        break
+                    file.write(data)
+                    if sha256:
+                        hasher.update(data)
+                    block_count += 1
+                    callback._download_progress(block_count, block_size)
+        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:
@@ -251,8 +245,12 @@
                 continue
         break
     # Delete file if cancelled, it may be a partial file.
+    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
     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-08-01 20:59:41 +0000
+++ openlp/core/common/languagemanager.py	2017-09-18 20:44:53 +0000
@@ -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:>2d} {name}'.format(count=counter + 1, name=LanguageManager.language_name(qmf))] = name
 
     @staticmethod
     def get_qm_list():

=== modified file 'openlp/core/common/registry.py'
--- openlp/core/common/registry.py	2017-03-03 19:27:31 +0000
+++ openlp/core/common/registry.py	2017-09-18 20:44:53 +0000
@@ -143,7 +143,7 @@
                     log.exception('Exception for function {function}'.format(function=function))
         else:
             trace_error_handler(log)
-            log.error("Event {event} called but not registered".format(event=event))
+            log.exception('Event {event} called but not registered'.format(event=event))
         return results
 
     def get_flag(self, key):

=== modified file 'openlp/core/common/uistrings.py'
--- openlp/core/common/uistrings.py	2017-08-03 17:54:40 +0000
+++ openlp/core/common/uistrings.py	2017-09-18 20:44:53 +0000
@@ -88,9 +88,6 @@
         self.Error = translate('OpenLP.Ui', 'Error')
         self.Export = translate('OpenLP.Ui', 'Export')
         self.File = translate('OpenLP.Ui', 'File')
-        self.FileNotFound = translate('OpenLP.Ui', 'File Not Found')
-        self.FileNotFoundMessage = translate('OpenLP.Ui',
-                                             'File {name} not found.\nPlease try selecting it individually.')
         self.FontSizePtUnit = translate('OpenLP.Ui', 'pt', 'Abbreviated font pointsize unit')
         self.Help = translate('OpenLP.Ui', 'Help')
         self.Hours = translate('OpenLP.Ui', 'h', 'The abbreviated unit for hours')

=== modified file 'openlp/core/lib/__init__.py'
--- openlp/core/lib/__init__.py	2017-08-25 20:03:25 +0000
+++ openlp/core/lib/__init__.py	2017-09-18 20:44:53 +0000
@@ -32,6 +32,7 @@
 from PyQt5 import QtCore, QtGui, Qt, QtWidgets
 
 from openlp.core.common import translate
+from openlp.core.common.path import Path
 
 log = logging.getLogger(__name__ + '.__init__')
 
@@ -125,10 +126,11 @@
     Build a QIcon instance from an existing QIcon, a resource location, or a physical file location. If the icon is a
     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.
+    :param QtGui.QIcon | Path | QtGui.QIcon | str icon:
+        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.
     :return: The build icon.
+    :rtype: QtGui.QIcon
     """
     if isinstance(icon, QtGui.QIcon):
         return icon
@@ -136,6 +138,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:
@@ -217,14 +221,15 @@
     Validates whether an file's thumb still exists and if is up to date. **Note**, you must **not** call this function,
     before checking the existence of the file.
 
-    :param file_path: The path to the file. The file **must** exist!
-    :param thumb_path: The path to the thumb.
-    :return: True, False if the image has changed since the thumb was created.
+    :param openlp.core.common.path.Path file_path: The path to the file. The file **must** exist!
+    :param openlp.core.common.path.Path thumb_path: The path to the thumb.
+    :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
 
 

=== modified file 'openlp/core/lib/mediamanageritem.py'
--- openlp/core/lib/mediamanageritem.py	2017-08-26 15:06:11 +0000
+++ openlp/core/lib/mediamanageritem.py	2017-09-18 20:44:53 +0000
@@ -359,10 +359,8 @@
         :param files: The files to be loaded.
         :param target_group: The QTreeWidgetItem of the group that will be the parent of the added files
         """
-        names = []
         full_list = []
         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))
         duplicates_found = False
         files_added = False

=== added file 'openlp/core/lib/shutil.py'
--- openlp/core/lib/shutil.py	1970-01-01 00:00:00 +0000
+++ openlp/core/lib/shutil.py	2017-09-18 20:44:53 +0000
@@ -0,0 +1,112 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2017 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+""" Patch the shutil methods we use so they accept and return Path objects"""
+import shutil
+
+from openlp.core.common.path import path_to_str, str_to_path
+from openlp.core.lib import replace_params
+
+
+def copy(*args, **kwargs):
+    """
+    Wraps :func:`shutil.copy` so that we can accept Path objects.
+
+    :param src openlp.core.common.path.Path: Takes a Path object which is then converted to a str object
+    :param dst openlp.core.common.path.Path: Takes a Path object which is then converted to a str object
+    :return: Converts the str object received from :func:`shutil.copy` to a Path or NoneType object
+    :rtype: openlp.core.common.path.Path | None
+
+    See the following link for more information on the other parameters:
+        https://docs.python.org/3/library/shutil.html#shutil.copy
+    """
+
+    args, kwargs = replace_params(args, kwargs, ((0, 'src', path_to_str), (1, 'dst', path_to_str)))
+
+    return str_to_path(shutil.copy(*args, **kwargs))
+
+
+def copyfile(*args, **kwargs):
+    """
+    Wraps :func:`shutil.copyfile` so that we can accept Path objects.
+
+    :param openlp.core.common.path.Path src: Takes a Path object which is then converted to a str object
+    :param openlp.core.common.path.Path dst: Takes a Path object which is then converted to a str object
+    :return: Converts the str object received from :func:`shutil.copyfile` to a Path or NoneType object
+    :rtype: openlp.core.common.path.Path | None
+
+    See the following link for more information on the other parameters:
+        https://docs.python.org/3/library/shutil.html#shutil.copyfile
+    """
+
+    args, kwargs = replace_params(args, kwargs, ((0, 'src', path_to_str), (1, 'dst', path_to_str)))
+
+    return str_to_path(shutil.copyfile(*args, **kwargs))
+
+
+def copytree(*args, **kwargs):
+    """
+    Wraps :func:shutil.copytree` so that we can accept Path objects.
+
+    :param openlp.core.common.path.Path src : Takes a Path object which is then converted to a str object
+    :param openlp.core.common.path.Path dst: Takes a Path object which is then converted to a str object
+    :return: Converts the str object received from :func:`shutil.copytree` to a Path or NoneType object
+    :rtype: openlp.core.common.path.Path | None
+
+    See the following link for more information on the other parameters:
+        https://docs.python.org/3/library/shutil.html#shutil.copytree
+    """
+
+    args, kwargs = replace_params(args, kwargs, ((0, 'src', path_to_str), (1, 'dst', path_to_str)))
+
+    return str_to_path(shutil.copytree(*args, **kwargs))
+
+
+def rmtree(*args, **kwargs):
+    """
+    Wraps :func:shutil.rmtree` so that we can accept Path objects.
+
+    :param openlp.core.common.path.Path path: Takes a Path object which is then converted to a str object
+    :return: Passes the return from :func:`shutil.rmtree` back
+    :rtype: None
+
+    See the following link for more information on the other parameters:
+        https://docs.python.org/3/library/shutil.html#shutil.rmtree
+    """
+
+    args, kwargs = replace_params(args, kwargs, ((0, 'path', path_to_str),))
+
+    return shutil.rmtree(*args, **kwargs)
+
+
+def which(*args, **kwargs):
+    """
+    Wraps :func:shutil.which` so that it return a Path objects.
+
+    :rtype: openlp.core.common.Path
+
+    See the following link for more information on the other parameters:
+        https://docs.python.org/3/library/shutil.html#shutil.which
+    """
+    file_name = shutil.which(*args, **kwargs)
+    if file_name:
+        return str_to_path(file_name)
+    return None

=== modified file 'openlp/core/ui/firsttimeform.py'
--- openlp/core/ui/firsttimeform.py	2017-08-25 20:03:25 +0000
+++ openlp/core/ui/firsttimeform.py	2017-09-18 20:44:53 +0000
@@ -563,7 +563,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 = Path(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,7 +576,7 @@
                 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),
+                                    Path(bibles_destination, bible),
                                     sha256):
                     missed_files.append('Bible: {name}'.format(name=bible))
             bibles_iterator += 1
@@ -588,7 +588,7 @@
                 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),
+                                    Path(themes_destination, theme),
                                     sha256):
                     missed_files.append('Theme: {name}'.format(name=theme))
         if missed_files:

=== modified file 'openlp/core/ui/lib/wizard.py'
--- openlp/core/ui/lib/wizard.py	2017-08-26 15:06:11 +0000
+++ openlp/core/ui/lib/wizard.py	2017-09-18 20:44:53 +0000
@@ -310,7 +310,7 @@
         """
         folder_path = FileDialog.getExistingDirectory(
             self, title, Settings().value(self.plugin.settings_section + '/' + setting_name),
-            QtWidgets.QFileDialog.ShowDirsOnly)
+            FileDialog.ShowDirsOnly)
         if folder_path:
             editbox.setText(str(folder_path))
             Settings().setValue(self.plugin.settings_section + '/' + setting_name, folder_path)

=== modified file 'openlp/core/ui/mainwindow.py'
--- openlp/core/ui/mainwindow.py	2017-09-03 10:18:14 +0000
+++ openlp/core/ui/mainwindow.py	2017-09-18 20:44:53 +0000
@@ -42,6 +42,7 @@
 from openlp.core.common.path import Path, path_to_str, str_to_path
 from openlp.core.common.versionchecker import get_application_version
 from openlp.core.lib import Renderer, PluginManager, ImageManager, PluginStatus, ScreenList, build_icon
+from openlp.core.lib.shutil import copyfile
 from openlp.core.lib.ui import create_action
 from openlp.core.ui import AboutForm, SettingsForm, ServiceManager, ThemeManager, LiveController, PluginForm, \
     ShortcutListForm, FormattingTagForm, PreviewController
@@ -848,12 +849,12 @@
                                                 QtWidgets.QMessageBox.No)
         if answer == QtWidgets.QMessageBox.No:
             return
-        import_file_name, filter_used = QtWidgets.QFileDialog.getOpenFileName(
+        import_file_path, filter_used = FileDialog.getOpenFileName(
             self,
             translate('OpenLP.MainWindow', 'Import settings'),
-            '',
+            None,
             translate('OpenLP.MainWindow', 'OpenLP Settings (*.conf)'))
-        if not import_file_name:
+        if import_file_path is None:
             return
         setting_sections = []
         # Add main sections.
@@ -871,12 +872,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')
-        check_directory_exists(Path(temp_directory))
-        temp_config = os.path.join(temp_directory, os.path.basename(import_file_name))
-        shutil.copyfile(import_file_name, temp_config)
+        temp_dir_path = Path(gettempdir(), 'openlp')
+        check_directory_exists(temp_dir_path)
+        temp_config_path = temp_dir_path / 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)
 
         log.info('hook upgrade_plugin_settings')
         self.plugin_manager.hook_upgrade_plugin_settings(import_settings)
@@ -920,7 +921,7 @@
                 settings.setValue('{key}'.format(key=section_key), value)
         now = datetime.now()
         settings.beginGroup(self.header_section)
-        settings.setValue('file_imported', import_file_name)
+        settings.setValue('file_imported', import_file_path)
         settings.setValue('file_date_imported', now.strftime("%Y-%m-%d %H:%M"))
         settings.endGroup()
         settings.sync()

=== modified file 'openlp/core/ui/servicemanager.py'
--- openlp/core/ui/servicemanager.py	2017-08-26 15:06:11 +0000
+++ openlp/core/ui/servicemanager.py	2017-09-18 20:44:53 +0000
@@ -366,16 +366,20 @@
         """
         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 openlp.core.common.path.Path file_path: The service file name
+        :rtype: None
         """
-        self._file_name = str(file_name)
+        self._file_name = path_to_str(file_path)
         self.main_window.set_service_modified(self.is_modified(), self.short_file_name())
-        Settings().setValue('servicemanager/last file', Path(file_name))
-        self._save_lite = self._file_name.endswith('.oszl')
+        Settings().setValue('servicemanager/last file', file_path)
+        if file_path and file_path.suffix() == '.oszl':
+            self._save_lite = True
+        else:
+            self._save_lite = False
 
     def file_name(self):
         """
@@ -474,7 +478,7 @@
         """
         self.service_manager_list.clear()
         self.service_items = []
-        self.set_file_name('')
+        self.set_file_name(None)
         self.service_id += 1
         self.set_modified(False)
         Settings().setValue('servicemanager/last file', None)
@@ -695,27 +699,23 @@
             default_file_name = format_time(default_pattern, local_time)
         else:
             default_file_name = ''
-        directory = path_to_str(Settings().value(self.main_window.service_manager_settings_section + '/last directory'))
-        path = os.path.join(directory, default_file_name)
+        directory_path = Settings().value(self.main_window.service_manager_settings_section + '/last directory')
+        file_path = directory_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,
+            file_path, filter_used = FileDialog.getSaveFileName(
+                self.main_window, UiStrings().SaveService, file_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,
+            file_path, filter_used = FileDialog.getSaveFileName(
+                self.main_window, UiStrings().SaveService, file_path,
                 translate('OpenLP.ServiceManager', 'OpenLP Service Files (*.osz);;'))
-        if not file_name:
+        if not file_path:
             return False
-        if os.path.splitext(file_name)[1] == '':
-            file_name += '.osz'
-        else:
-            ext = os.path.splitext(file_name)[1]
-            file_name.replace(ext, '.osz')
-        self.set_file_name(file_name)
+        file_path.with_suffix('.osz')
+        self.set_file_name(file_path)
         self.decide_save_method()
 
     def decide_save_method(self, field=None):
@@ -772,7 +772,7 @@
                     return
                 file_to.close()
                 self.new_file()
-                self.set_file_name(file_name)
+                self.set_file_name(str_to_path(file_name))
                 self.main_window.display_progress_bar(len(items))
                 self.process_service_items(items)
                 delete_file(Path(p_file))

=== modified file 'openlp/core/ui/thememanager.py'
--- openlp/core/ui/thememanager.py	2017-08-26 16:50:54 +0000
+++ openlp/core/ui/thememanager.py	2017-09-18 20:44:53 +0000
@@ -483,7 +483,7 @@
                     name = text_name
                 thumb = os.path.join(self.thumb_path, '{name}.png'.format(name=text_name))
                 item_name = QtWidgets.QListWidgetItem(name)
-                if validate_thumb(theme, thumb):
+                if validate_thumb(Path(theme), Path(thumb)):
                     icon = build_icon(thumb)
                 else:
                     icon = create_thumb(theme, thumb)

=== modified file 'openlp/plugins/images/lib/mediaitem.py'
--- openlp/plugins/images/lib/mediaitem.py	2017-08-26 15:06:11 +0000
+++ openlp/plugins/images/lib/mediaitem.py	2017-09-18 20:44:53 +0000
@@ -360,7 +360,7 @@
             if not os.path.exists(image_file.filename):
                 icon = build_icon(':/general/general_delete.png')
             else:
-                if validate_thumb(image_file.filename, thumb):
+                if validate_thumb(Path(image_file.filename), Path(thumb)):
                     icon = build_icon(thumb)
                 else:
                     icon = create_thumb(image_file.filename, thumb)

=== modified file 'openlp/plugins/presentations/lib/impresscontroller.py'
--- openlp/plugins/presentations/lib/impresscontroller.py	2017-08-25 20:03:25 +0000
+++ openlp/plugins/presentations/lib/impresscontroller.py	2017-09-18 20:44:53 +0000
@@ -32,11 +32,14 @@
 # http://nxsy.org/comparing-documents-with-openoffice-and-python
 
 import logging
-import os
 import time
 
-from openlp.core.common import is_win, Registry, delete_file
-from openlp.core.common.path import Path
+from PyQt5 import QtCore
+
+from openlp.core.common import Registry, delete_file, get_uno_command, get_uno_instance, is_win
+from openlp.core.lib import ScreenList
+from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument, \
+    TextType
 
 if is_win():
     from win32com.client import Dispatch
@@ -55,14 +58,6 @@
     except ImportError:
         uno_available = False
 
-from PyQt5 import QtCore
-
-from openlp.core.lib import ScreenList
-from openlp.core.common import get_uno_command, get_uno_instance
-from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument, \
-    TextType
-
-
 log = logging.getLogger(__name__)
 
 
@@ -203,12 +198,15 @@
     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 openlp.core.common.path.Path document_path: File path for the document to load
+        :rtype: None
         """
         log.debug('Init Presentation OpenOffice')
-        super(ImpressDocument, self).__init__(controller, presentation)
+        super().__init__(controller, document_path)
         self.document = None
         self.presentation = None
         self.control = None
@@ -225,10 +223,9 @@
             if desktop is None:
                 self.controller.start_process()
                 desktop = self.controller.get_com_desktop()
-            url = 'file:///' + self.file_path.replace('\\', '/').replace(':', '|').replace(' ', '%20')
         else:
             desktop = self.controller.get_uno_desktop()
-            url = uno.systemPathToFileUrl(self.file_path)
+        url = self.file_path.as_uri()
         if desktop is None:
             return False
         self.desktop = desktop
@@ -254,11 +251,8 @@
         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())
+        temp_folder_path = self.get_temp_folder()
+        thumb_dir_url = temp_folder_path.as_uri()
         properties = []
         properties.append(self.create_property('FilterName', 'impress_png_Export'))
         properties = tuple(properties)
@@ -266,17 +260,17 @@
         pages = doc.getDrawPages()
         if not pages:
             return
-        if not os.path.isdir(self.get_temp_folder()):
-            os.makedirs(self.get_temp_folder())
+        if not temp_folder_path.is_dir():
+            temp_folder_path.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 = temp_folder_path / '{number:d}.png'.format(number=index + 1)
             try:
                 doc.storeToURL(url_path, properties)
                 self.convert_thumbnail(path, index + 1)
-                delete_file(Path(path))
+                delete_file(path)
             except ErrorCodeIOException as exception:
                 log.exception('ERROR! ErrorCodeIOException {error:d}'.format(error=exception.ErrCode))
             except:

=== modified file 'openlp/plugins/presentations/lib/mediaitem.py'
--- openlp/plugins/presentations/lib/mediaitem.py	2017-08-26 15:06:11 +0000
+++ openlp/plugins/presentations/lib/mediaitem.py	2017-09-18 20:44:53 +0000
@@ -19,15 +19,13 @@
 # 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 PyQt5 import QtCore, QtGui, QtWidgets
 
 from openlp.core.common import Registry, Settings, UiStrings, translate
 from openlp.core.common.languagemanager import get_locale_key
-from openlp.core.common.path import path_to_str
+from openlp.core.common.path import Path, path_to_str, str_to_path
 from openlp.core.lib import MediaManagerItem, ItemCapabilities, ServiceItemContext,\
     build_icon, check_item_selected, create_thumb, validate_thumb
 from openlp.core.lib.ui import critical_error_message_box, create_horizontal_adjusting_combo_box
@@ -128,7 +126,7 @@
         """
         self.list_view.setIconSize(QtCore.QSize(88, 50))
         file_paths = Settings().value(self.settings_section + '/presentations files')
-        self.load_list([path_to_str(file) for file in file_paths], initial_load=True)
+        self.load_list([path_to_str(path) for path in file_paths], initial_load=True)
         self.populate_display_types()
 
     def populate_display_types(self):
@@ -152,54 +150,57 @@
         else:
             self.presentation_widget.hide()
 
-    def load_list(self, files, target_group=None, initial_load=False):
+    def load_list(self, file_paths, target_group=None, initial_load=False):
         """
         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.
+
+        :param list[openlp.core.common.path.Path] file_paths: List of file paths to add to the media manager.
         """
-        current_list = self.get_file_list()
-        titles = [file_path.name for file_path in current_list]
+        file_paths = [str_to_path(filename) for filename in file_paths]
+        current_paths = self.get_file_list()
+        titles = [file_path.name for file_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, path_to_str(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_path)
                 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(Path(preview_path), Path(thumbnail_path)):
+                            icon = build_icon(thumbnail_path)
                         else:
-                            icon = create_thumb(preview, thumb)
+                            icon = create_thumb(str(preview_path), str(thumbnail_path))
                 else:
                     if initial_load:
                         icon = build_icon(':/general/general_delete.png')
@@ -208,10 +209,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, path_to_str(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()
@@ -228,8 +229,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 = str_to_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:
@@ -237,30 +238,29 @@
             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 clean_for_update: Only clean thumbnails if update is needed
-        :return: None
+        :param openlp.core.common.path.Path file_path: File path of the presention to clean up after
+        :param bool clean_for_update: Only clean thumbnails if update is needed
+        :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()
                 doc.close_presentation()
 
     def generate_slide_data(self, service_item, item=None, xml_version=False, remote=False,
-                            context=ServiceItemContext.Service, presentation_file=None):
+                            context=ServiceItemContext.Service, file_path=None):
         """
         Generate the slide data. Needs to be implemented by the plugin.
 
@@ -276,10 +276,9 @@
             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:]
+        if file_path is None:
+            file_path = str_to_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)
@@ -292,29 +291,28 @@
             # 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)
-                service_item.title = name
-                if os.path.exists(filename):
-                    processor = self.find_controller_by_type(filename)
+                if file_path is None:
+                    file_path = str_to_path(bitem.data(QtCore.Qt.UserRole))
+                path, file_name = file_path.parent, file_path.name
+                service_item.title = file_name
+                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(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{number:d}.png'.format(number=i)
+                    while image_path.is_file():
+                        service_item.add_from_image(str(image_path), file_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
@@ -324,34 +322,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)
-                service_item.title = name
-                if os.path.exists(filename):
+                file_path = str_to_path(bitem.data(QtCore.Qt.UserRole))
+                path, file_name = file_path.parent, file_path.name
+                service_item.title = file_name
+                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(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:
@@ -359,9 +357,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(str(path), file_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:
@@ -371,7 +369,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
@@ -379,18 +377,20 @@
                         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):
+    def find_controller_by_type(self, file_path):
         """
         Determine the default application controller to use for the selected file type. This is used if "Automatic" is
         set as the preferred controller. Find the first (alphabetic) enabled controller which "supports" the extension.
         If none found, then look for a controller which "also supports" it instead.
 
-        :param filename: The file name
+        :param openlp.core.common.path.Path file_path: The file path
+        :return: The default application controller for this file type, or None if not supported
+        :rtype: PresentationController
         """
-        file_type = os.path.splitext(filename)[1][1:]
+        file_type = file_path.suffix[1:]
         if not file_type:
             return None
         for controller in self.controllers:

=== 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-09-18 20:44:53 +0000
@@ -19,16 +19,15 @@
 # with this program; if not, write to the Free Software Foundation, Inc., 59  #
 # Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
 ###############################################################################
-
+import copy
 import logging
-import copy
-import os
 
 from PyQt5 import QtCore
 
 from openlp.core.common import Registry, Settings
+from openlp.core.common.path import Path
+from openlp.core.lib import ServiceItemContext
 from openlp.core.ui import HideMode
-from openlp.core.lib import ServiceItemContext
 from openlp.plugins.presentations.lib.pdfcontroller import PDF_CONTROLLER_FILETYPES
 
 log = logging.getLogger(__name__)
@@ -325,21 +324,25 @@
         is_live = message[1]
         item = message[0]
         hide_mode = message[2]
-        file = item.get_frame_path()
+        file_path = Path(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
         # the conversion has already been done at this point.
-        file_type = os.path.splitext(file.lower())[1][1:]
+        file_type = file_path.suffix.lower()[1:]
         if file_type in PDF_CONTROLLER_FILETYPES:
-            log.debug('Converting from pdf/xps/oxps to images for serviceitem with file {name}'.format(name=file))
+            log.debug('Converting from pdf/xps/oxps to images for serviceitem with file {name}'.format(name=file_path))
             # Create a copy of the original item, and then clear the original item so it can be filled with images
             item_cpy = copy.copy(item)
             item.__init__(None)
             if is_live:
-                self.media_item.generate_slide_data(item, item_cpy, False, False, ServiceItemContext.Live, file)
+                # TODO: To Path object
+                self.media_item.generate_slide_data(item, item_cpy, False, False, ServiceItemContext.Live,
+                                                    str(file_path))
             else:
-                self.media_item.generate_slide_data(item, item_cpy, False, False, ServiceItemContext.Preview, file)
+                # TODO: To Path object
+                self.media_item.generate_slide_data(item, item_cpy, False, False, ServiceItemContext.Preview,
+                                                    str(file_path))
             # Some of the original serviceitem attributes is needed in the new serviceitem
             item.footer = item_cpy.footer
             item.from_service = item_cpy.from_service
@@ -352,13 +355,13 @@
             self.handler = None
         else:
             if self.handler == self.media_item.automatic:
-                self.handler = self.media_item.find_controller_by_type(file)
+                self.handler = self.media_item.find_controller_by_type(file_path)
                 if not self.handler:
                     return
             else:
-                # the saved handler is not present so need to use one based on file suffix.
+                # the saved handler is not present so need to use one based on file_path suffix.
                 if not self.controllers[self.handler].available:
-                    self.handler = self.media_item.find_controller_by_type(file)
+                    self.handler = self.media_item.find_controller_by_type(file_path)
                     if not self.handler:
                         return
         if is_live:
@@ -370,7 +373,7 @@
         if self.handler is None:
             self.controller = controller
         else:
-            controller.add_handler(self.controllers[self.handler], file, hide_mode, message[3])
+            controller.add_handler(self.controllers[self.handler], file_path, hide_mode, message[3])
             self.timer.start()
 
     def slide(self, message):

=== modified file 'openlp/plugins/presentations/lib/pdfcontroller.py'
--- openlp/plugins/presentations/lib/pdfcontroller.py	2017-08-26 15:06:11 +0000
+++ openlp/plugins/presentations/lib/pdfcontroller.py	2017-09-18 20:44:53 +0000
@@ -23,13 +23,13 @@
 import os
 import logging
 import re
-from shutil import which
 from subprocess import check_output, CalledProcessError
 
 from openlp.core.common import AppLocation, check_binary_exists
 from openlp.core.common import Settings, is_win
 from openlp.core.common.path import Path, path_to_str
 from openlp.core.lib import ScreenList
+from openlp.core.lib.shutil import which
 from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
 
 if is_win():
@@ -66,11 +66,12 @@
         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 openlp.core.common.path.Path program_path: The full path to the binary to check.
         :return: Type of the binary, 'gs' if ghostscript, 'mudraw' if mudraw, None if invalid.
+        :rtype: str | None
         """
         program_type = None
-        runlog = check_binary_exists(Path(program_path))
+        runlog = check_binary_exists(program_path)
         # Analyse the output to see it the program is mudraw, ghostscript or neither
         for line in runlog.splitlines():
             decoded_line = line.decode()
@@ -107,30 +108,29 @@
         :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 = path_to_str(Settings().value('presentations/pdf_program'))
-            program_type = self.process_check_binary(pdf_program)
+            program_path = Settings().value('presentations/pdf_program')
+            program_type = self.process_check_binary(program_path)
             if program_type == 'gs':
-                self.gsbin = pdf_program
+                self.gsbin = program_path
             elif program_type == 'mudraw':
-                self.mudrawbin = pdf_program
+                self.mudrawbin = program_path
             elif program_type == 'mutool':
-                self.mutoolbin = pdf_program
+                self.mutoolbin = program_path
         else:
             # Fallback to autodetection
-            application_path = str(AppLocation.get_directory(AppLocation.AppDir))
+            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 = str(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
@@ -143,11 +143,11 @@
                         self.gsbin = which('gs')
                 # 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 = str(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')
+                    application_path = AppLocation.get_directory(AppLocation.AppDir)
+                    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
@@ -172,12 +172,15 @@
     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 openlp.core.common.path.Path document_path: Path to the document to load
+        :rtype: None
         """
         log.debug('Init Presentation Pdf')
-        PresentationDocument.__init__(self, controller, presentation)
+        super().__init__(controller, document_path)
         self.presentation = None
         self.blanked = False
         self.hidden = False
@@ -200,13 +203,13 @@
         :return: The resolution dpi to be used.
         """
         # Use a postscript script to get size of the pdf. It is assumed that all pages have same size
-        gs_resolution_script = str(AppLocation.get_directory(
-            AppLocation.PluginsDir)) + '/presentations/lib/ghostscript_get_resolution.ps'
+        gs_resolution_script = AppLocation.get_directory(
+            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],
+            runlog = check_output([str(self.controller.gsbin), '-dNOPAUSE', '-dNODISPLAY', '-dBATCH',
+                                   '-sFile={file_path}'.format(file_path=self.file_path), str(gs_resolution_script)],
                                   startupinfo=self.startupinfo)
         except CalledProcessError as e:
             log.debug(' '.join(e.cmd))
@@ -240,46 +243,47 @@
         :return: True is loading succeeded, otherwise False.
         """
         log.debug('load_presentation pdf')
+        temp_dir_path = self.get_temp_folder()
         # 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 (temp_dir_path / 'mainslide001.png').is_file():
+            created_files = sorted(temp_dir_path.glob('*'))
+            for image_path in created_files:
+                if image_path.is_file():
+                    self.image_files.append(image_path)
             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 temp_dir_path.is_dir():
+                temp_dir_path.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],
+                runlog = check_output([str(self.controller.mudrawbin), '-w', str(size.width()),
+                                       '-h', str(size.height()),
+                                       '-o', str(temp_dir_path / 'mainslide%03d.png'), str(self.file_path)],
                                       startupinfo=self.startupinfo)
             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],
+                runlog = check_output([str(self.controller.mutoolbin), 'draw', '-w', str(size.width()),
+                                       '-h', str(size.height()), '-o', str(temp_dir_path / 'mainslide%03d.png'),
+                                       str(self.file_path)],
                                       startupinfo=self.startupinfo)
             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',
-                                       '-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))
+                runlog = check_output([str(self.controller.gsbin), '-dSAFER', '-dNOPAUSE', '-dBATCH', '-sDEVICE=png16m',
+                                       '-r{res}'.format(res=resolution), '-dTextAlphaBits=4', '-dGraphicsAlphaBits=4',
+                                       '-sOutputFile={output}'.format(output=temp_dir_path / 'mainslide%03d.png'),
+                                       str(self.file_path)], startupinfo=self.startupinfo)
+            created_files = sorted(temp_dir_path.glob('*'))
+            for image_path in created_files:
+                if image_path.is_file():
+                    self.image_files.append(image_path)
         except Exception as e:
-            log.debug(e)
-            log.debug(runlog)
+            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-07-04 23:13:51 +0000
+++ openlp/plugins/presentations/lib/powerpointcontroller.py	2017-09-18 20:44:53 +0000
@@ -120,15 +120,16 @@
     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 openlp.core.common.path.Path document_path: Path to the document to load
+        :rtype: None
         """
         log.debug('Init Presentation Powerpoint')
-        super(PowerpointDocument, self).__init__(controller, presentation)
+        super().__init__(controller, document_path)
         self.presentation = None
         self.index_map = {}
         self.slide_count = 0
@@ -145,7 +146,7 @@
         try:
             if not self.controller.process:
                 self.controller.start_process()
-            self.controller.process.Presentations.Open(os.path.normpath(self.file_path), False, False, False)
+            self.controller.process.Presentations.Open(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 +178,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)
+                    str(self.get_thumbnail_folder() / 'slide{key:d}.png'.format(key=key)), 'png', 320, 240)
                 key += 1
         self.slide_count = key - 1
 
@@ -363,9 +364,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-08-01 20:59:41 +0000
+++ openlp/plugins/presentations/lib/pptviewcontroller.py	2017-09-18 20:44:53 +0000
@@ -85,9 +85,9 @@
             if self.process:
                 return
             log.debug('start PPTView')
-            dll_path = os.path.join(str(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,15 @@
     """
     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 openlp.core.common.path.Path document_path: File path to the document to load
+        :rtype: None
         """
         log.debug('Init Presentation PowerPoint')
-        super(PptviewDocument, self).__init__(controller, presentation)
+        super().__init__(controller, document_path)
         self.presentation = None
         self.ppt_id = None
         self.blanked = False
@@ -121,17 +124,16 @@
         the background PptView task started earlier.
         """
         log.debug('LoadPresentation')
-        temp_folder = self.get_temp_folder()
+        temp_path = 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)
-        preview_path = os.path.join(temp_folder, 'slide')
+        preview_path = temp_path / 'slide'
         # Ensure that the paths are null terminated
-        byte_file_path = self.file_path.encode('utf-16-le') + b'\0'
-        preview_path = preview_path.encode('utf-16-le') + b'\0'
-        if not os.path.isdir(temp_folder):
-            os.makedirs(temp_folder)
-        self.ppt_id = self.controller.process.OpenPPT(byte_file_path, None, rect, preview_path)
+        file_path_utf16 = str(self.file_path).encode('utf-16-le') + b'\0'
+        preview_path_utf16 = str(preview_path).encode('utf-16-le') + b'\0'
+        if not temp_path.is_dir():
+            temp_path.mkdir(parents=True)
+        self.ppt_id = self.controller.process.OpenPPT(file_path_utf16, None, rect, preview_path_utf16)
         if self.ppt_id >= 0:
             self.create_thumbnails()
             self.stop_presentation()
@@ -148,7 +150,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:d}.bmp'.format(index=idx + 1)
             self.convert_thumbnail(path, idx + 1)
 
     def create_titles_and_notes(self):
@@ -161,13 +163,12 @@
         """
         titles = None
         notes = None
-        filename = os.path.normpath(self.file_path)
         # let's make sure we have a valid zipped presentation
-        if os.path.exists(filename) and zipfile.is_zipfile(filename):
+        if self.file_path.exists() and zipfile.is_zipfile(str(self.file_path)):
             namespaces = {"p": "http://schemas.openxmlformats.org/presentationml/2006/main";,
                           "a": "http://schemas.openxmlformats.org/drawingml/2006/main"}
             # open the file
-            with zipfile.ZipFile(filename) as zip_file:
+            with zipfile.ZipFile(str(self.file_path)) as zip_file:
                 # find the presentation.xml to get the slide count
                 with zip_file.open('ppt/presentation.xml') as pres:
                     tree = ElementTree.parse(pres)

=== modified file 'openlp/plugins/presentations/lib/presentationcontroller.py'
--- openlp/plugins/presentations/lib/presentationcontroller.py	2017-08-25 20:03:25 +0000
+++ openlp/plugins/presentations/lib/presentationcontroller.py	2017-09-18 20:44:53 +0000
@@ -19,16 +19,14 @@
 # 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
-import shutil
 
 from PyQt5 import QtCore
 
 from openlp.core.common import Registry, AppLocation, Settings, check_directory_exists, md5_hash
 from openlp.core.common.path import Path
 from openlp.core.lib import create_thumb, validate_thumb
+from openlp.core.lib.shutil import rmtree
 
 log = logging.getLogger(__name__)
 
@@ -86,20 +84,27 @@
         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 controller:
+        :param openlp.core.common.path.Path document_path: Path to the document to load.
+        :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 openlp.core.common.path.Path document_path: Path to the document to load.
+        :rtype: None
         """
         self.slide_number = 0
-        self.file_path = name
-        check_directory_exists(Path(self.get_thumbnail_folder()))
+        self.file_path = document_path
+        check_directory_exists(self.get_thumbnail_folder())
 
     def load_presentation(self):
         """
@@ -116,49 +121,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())
+            thumbnail_folder_path = self.get_thumbnail_folder()
+            temp_folder_path = self.get_temp_folder()
+            if thumbnail_folder_path.exists():
+                rmtree(thumbnail_folder_path)
+            if temp_folder_path.exists():
+                rmtree(temp_folder_path)
         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: openlp.core.common.path.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
+        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: openlp.core.common.path.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
+        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(Path(self.file_path), Path(last_image_path))
 
     def close_presentation(self):
         """
@@ -241,25 +251,31 @@
         """
         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 openlp.core.common.path.Path image_path: Path to the image to create a thumb nail of
+        :param int index: The index of the slide to create the thumbnail for.
+        :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(str(image_path), str(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:
+        :param int slide_no: The slide an image is required for, starting at 1
+        :param bool check_exists: Check if the generated path exists
+        :return: The path, or None if the :param:`check_exists` is True and the file does not exist
+        :rtype: openlp.core.common.path.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
@@ -302,44 +318,38 @@
         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 list[str] titles: The titles to save
+        :param list[str] notes: The notes to save
+        :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):
@@ -416,12 +426,11 @@
         self.document_class = document_class
         self.settings_section = self.plugin.settings_section
         self.available = None
-        self.temp_folder = os.path.join(str(AppLocation.get_section_data_path(self.settings_section)), name)
-        self.thumbnail_folder = os.path.join(
-            str(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(Path(self.thumbnail_folder))
-        check_directory_exists(Path(self.temp_folder))
+        check_directory_exists(self.thumbnail_folder)
+        check_directory_exists(self.temp_folder)
 
     def enabled(self):
         """
@@ -456,11 +465,15 @@
         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 openlp.core.common.path.Path document_path: Path to the document to load
+        :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-08-26 15:06:11 +0000
+++ openlp/plugins/presentations/lib/presentationtab.py	2017-09-18 20:44:53 +0000
@@ -38,7 +38,6 @@
         """
         Constructor
         """
-        self.parent = parent
         self.controllers = controllers
         super(PresentationTab, self).__init__(parent, title, visible_title, icon_path)
         self.activated = False
@@ -194,7 +193,7 @@
         pdf_program_path = self.program_path_edit.path
         enable_pdf_program = self.pdf_program_check_box.checkState()
         # If the given program is blank disable using the program
-        if not pdf_program_path:
+        if pdf_program_path is None:
             enable_pdf_program = 0
         if pdf_program_path != Settings().value(self.settings_section + '/pdf_program'):
             Settings().setValue(self.settings_section + '/pdf_program', pdf_program_path)
@@ -220,9 +219,11 @@
 
     def on_program_path_edit_path_changed(self, new_path):
         """
-        Select the mudraw or ghostscript binary that should be used.
+        Handle the `pathEditChanged` signal from program_path_edit
+
+        :param openlp.core.common.path.Path new_path: File path to the new program
+        :rtype: None
         """
-        new_path = path_to_str(new_path)
         if new_path:
             if not PdfController.process_check_binary(new_path):
                 critical_error_message_box(UiStrings().Error,

=== modified file 'openlp/plugins/remotes/deploy.py'
--- openlp/plugins/remotes/deploy.py	2017-08-12 20:58:16 +0000
+++ openlp/plugins/remotes/deploy.py	2017-09-18 20:44:53 +0000
@@ -64,6 +64,6 @@
     file_size = get_url_file_size('https://get.openlp.org/webclient/site.zip')
     callback.setRange(0, file_size)
     if url_get_file(callback, '{host}{name}'.format(host='https://get.openlp.org/webclient/', name='site.zip'),
-                    os.path.join(str(AppLocation.get_section_data_path('remotes')), 'site.zip'),
+                    AppLocation.get_section_data_path('remotes') / 'site.zip',
                     sha256=sha256):
         deploy_zipfile(str(AppLocation.get_section_data_path('remotes')), 'site.zip')

=== 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-09-18 20:44:53 +0000
@@ -25,10 +25,10 @@
 import csv
 import logging
 
-from PyQt5 import QtWidgets
-
 from openlp.core.common import Registry, translate
+from openlp.core.common.path import Path
 from openlp.core.lib.ui import critical_error_message_box
+from openlp.core.ui.lib.filedialog import FileDialog
 from openlp.plugins.songs.lib.db import Song
 
 
@@ -42,58 +42,55 @@
     """
     main_window = Registry().get('main_window')
     plugin = Registry().get('songs').plugin
-    report_file_name, filter_used = QtWidgets.QFileDialog.getSaveFileName(
+    report_file_path, filter_used = FileDialog.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 '
-                                                   'report. \nPlease select an existing path '
-                                                   'on your computer.')
+            translate('SongPlugin.ReportSongList', 'You have not set a valid output location for your report. \n'
+                                                   'Please select an existing path 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 file_handle:
+            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_path)
+            )
     except OSError as ose:
         Registry().get('application').set_normal_cursor()
         log.exception('Failed to write out song usage records')
@@ -101,6 +98,3 @@
                                    translate('SongPlugin.ReportSongList',
                                              'An error occurred while extracting: {error}'
                                              ).format(error=ose.strerror))
-    finally:
-        if file_handle:
-            file_handle.close()

=== modified file 'tests/functional/openlp_core_common/test_httputils.py'
--- tests/functional/openlp_core_common/test_httputils.py	2017-06-09 20:53:13 +0000
+++ tests/functional/openlp_core_common/test_httputils.py	2017-09-18 20:44:53 +0000
@@ -29,6 +29,7 @@
 from unittest.mock import MagicMock, patch
 
 from openlp.core.common.httputils import get_user_agent, get_web_page, get_url_file_size, url_get_file, ping
+from openlp.core.common.path import Path
 
 from tests.helpers.testmixin import TestMixin
 
@@ -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_lib/test_lib.py'
--- tests/functional/openlp_core_lib/test_lib.py	2017-08-25 20:03:25 +0000
+++ tests/functional/openlp_core_lib/test_lib.py	2017-09-18 20:44:53 +0000
@@ -595,61 +595,46 @@
         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
+        with patch.object(Path, 'exists', return_value=False) as mocked_path_exists:
+            file_path = Path('path', 'to', 'file')
+            thumb_path = Path('path', 'to', 'thumb')
 
             # 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'
+            thumb_path.exists.assert_called_once_with()
+            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]
+        with patch.object(Path, 'exists'), patch.object(Path, 'stat'):
+            # 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: we should have called a few functions, and the result should be True
-            # mocked_os.path.exists.assert_called_with(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_replace_params_no_params(self):
         """

=== added file 'tests/functional/openlp_core_lib/test_shutil.py'
--- tests/functional/openlp_core_lib/test_shutil.py	1970-01-01 00:00:00 +0000
+++ tests/functional/openlp_core_lib/test_shutil.py	2017-09-18 20:44:53 +0000
@@ -0,0 +1,170 @@
+import os
+from unittest import TestCase
+from unittest.mock import ANY, MagicMock, patch
+
+from openlp.core.common.path import Path
+from openlp.core.lib.shutil import copy, copyfile, copytree, rmtree, which
+
+
+class TestShutil(TestCase):
+    """
+    Tests for the :mod:`openlp.core.lib.shutil` module
+    """
+
+    def test_copy(self):
+        """
+        Test :func:`copy`
+        """
+        # GIVEN: A mocked `shutil.copy` which returns a test path as a string
+        with patch('openlp.core.lib.shutil.shutil.copy', return_value=os.path.join('destination', 'test', 'path')) \
+                as mocked_shutil_copy:
+
+            # WHEN: Calling :func:`copy` with the src and dst parameters as Path object types
+            result = copy(Path('source', 'test', 'path'), Path('destination', 'test', 'path'))
+
+            # THEN: :func:`shutil.copy` should have been called with the str equivalents of the Path objects.
+            #       :func:`copy` should return the str type result of calling :func:`shutil.copy` as a Path object.
+            mocked_shutil_copy.assert_called_once_with(os.path.join('source', 'test', 'path'),
+                                                       os.path.join('destination', 'test', 'path'))
+            self.assertEqual(result, Path('destination', 'test', 'path'))
+
+    def test_copy_follow_optional_params(self):
+        """
+        Test :func:`copy` when follow_symlinks is set to false
+        """
+        # GIVEN: A mocked `shutil.copy`
+        with patch('openlp.core.lib.shutil.shutil.copy', return_value='') as mocked_shutil_copy:
+
+            # WHEN: Calling :func:`copy` with :param:`follow_symlinks` set to False
+            copy(Path('source', 'test', 'path'), Path('destination', 'test', 'path'), follow_symlinks=False)
+
+            # THEN: :func:`shutil.copy` should have been called with :param:`follow_symlinks` set to false
+            mocked_shutil_copy.assert_called_once_with(ANY, ANY, follow_symlinks=False)
+
+    def test_copyfile(self):
+        """
+        Test :func:`copyfile`
+        """
+        # GIVEN: A mocked :func:`shutil.copyfile` which returns a test path as a string
+        with patch('openlp.core.lib.shutil.shutil.copyfile',
+                   return_value=os.path.join('destination', 'test', 'path')) as mocked_shutil_copyfile:
+
+            # WHEN: Calling :func:`copyfile` with the src and dst parameters as Path object types
+            result = copyfile(Path('source', 'test', 'path'), Path('destination', 'test', 'path'))
+
+            # THEN: :func:`shutil.copyfile` should have been called with the str equivalents of the Path objects.
+            #       :func:`copyfile` should return the str type result of calling :func:`shutil.copyfile` as a Path
+            #       object.
+            mocked_shutil_copyfile.assert_called_once_with(os.path.join('source', 'test', 'path'),
+                                                           os.path.join('destination', 'test', 'path'))
+            self.assertEqual(result, Path('destination', 'test', 'path'))
+
+    def test_copyfile_optional_params(self):
+        """
+        Test :func:`copyfile` when follow_symlinks is set to false
+        """
+        # GIVEN: A mocked :func:`shutil.copyfile`
+        with patch('openlp.core.lib.shutil.shutil.copyfile', return_value='') as mocked_shutil_copyfile:
+
+            # WHEN: Calling :func:`copyfile` with :param:`follow_symlinks` set to False
+            copyfile(Path('source', 'test', 'path'), Path('destination', 'test', 'path'), follow_symlinks=False)
+
+            # THEN: :func:`shutil.copyfile` should have been called with the optional parameters, with out any of the
+            #       values being modified
+            mocked_shutil_copyfile.assert_called_once_with(ANY, ANY, follow_symlinks=False)
+
+    def test_copytree(self):
+        """
+        Test :func:`copytree`
+        """
+        # GIVEN: A mocked :func:`shutil.copytree` which returns a test path as a string
+        with patch('openlp.core.lib.shutil.shutil.copytree',
+                   return_value=os.path.join('destination', 'test', 'path')) as mocked_shutil_copytree:
+
+            # WHEN: Calling :func:`copytree` with the src and dst parameters as Path object types
+            result = copytree(Path('source', 'test', 'path'), Path('destination', 'test', 'path'))
+
+            # THEN: :func:`shutil.copytree` should have been called with the str equivalents of the Path objects.
+            #       :func:`patches.copytree` should return the str type result of calling :func:`shutil.copytree` as a
+            #       Path object.
+            mocked_shutil_copytree.assert_called_once_with(os.path.join('source', 'test', 'path'),
+                                                           os.path.join('destination', 'test', 'path'))
+            self.assertEqual(result, Path('destination', 'test', 'path'))
+
+    def test_copytree_optional_params(self):
+        """
+        Test :func:`copytree` when optional parameters are passed
+        """
+        # GIVEN: A mocked :func:`shutil.copytree`
+        with patch('openlp.core.lib.shutil.shutil.copytree', return_value='') as mocked_shutil_copytree:
+            mocked_ignore = MagicMock()
+            mocked_copy_function = MagicMock()
+
+            # WHEN: Calling :func:`copytree` with the optional parameters set
+            copytree(Path('source', 'test', 'path'), Path('destination', 'test', 'path'), symlinks=True,
+                     ignore=mocked_ignore, copy_function=mocked_copy_function, ignore_dangling_symlinks=True)
+
+            # THEN: :func:`shutil.copytree` should have been called with the optional parameters, with out any of the
+            #       values being modified
+            mocked_shutil_copytree.assert_called_once_with(ANY, ANY, symlinks=True, ignore=mocked_ignore,
+                                                           copy_function=mocked_copy_function,
+                                                           ignore_dangling_symlinks=True)
+
+    def test_rmtree(self):
+        """
+        Test :func:`rmtree`
+        """
+        # GIVEN: A mocked :func:`shutil.rmtree`
+        with patch('openlp.core.lib.shutil.shutil.rmtree', return_value=None) as mocked_shutil_rmtree:
+
+            # WHEN: Calling :func:`rmtree` with the path parameter as Path object type
+            result = rmtree(Path('test', 'path'))
+
+            # THEN: :func:`shutil.rmtree` should have been called with the str equivalents of the Path object.
+            mocked_shutil_rmtree.assert_called_once_with(os.path.join('test', 'path'))
+            self.assertIsNone(result)
+
+    def test_rmtree_optional_params(self):
+        """
+        Test :func:`rmtree` when optional parameters are passed
+        """
+        # GIVEN: A mocked :func:`shutil.rmtree`
+        with patch('openlp.core.lib.shutil.shutil.rmtree', return_value='') as mocked_shutil_rmtree:
+            mocked_on_error = MagicMock()
+
+            # WHEN: Calling :func:`rmtree` with :param:`ignore_errors` set to True and `onerror` set to a mocked object
+            rmtree(Path('test', 'path'), ignore_errors=True, onerror=mocked_on_error)
+
+            # THEN: :func:`shutil.rmtree` should have been called with the optional parameters, with out any of the
+            #       values being modified
+            mocked_shutil_rmtree.assert_called_once_with(ANY, ignore_errors=True, onerror=mocked_on_error)
+
+    def test_which_no_command(self):
+        """
+        Test :func:`which` when the command is not found.
+        """
+        # GIVEN: A mocked :func:``shutil.which` when the command is not found.
+        with patch('openlp.core.lib.shutil.shutil.which', return_value=None) as mocked_shutil_which:
+
+            # WHEN: Calling :func:`which` with a command that does not exist.
+            result = which('no_command')
+
+            # THEN: :func:`shutil.which` should have been called with the command, and :func:`which` should return None.
+            mocked_shutil_which.assert_called_once_with('no_command')
+            self.assertIsNone(result)
+
+    def test_which_command(self):
+        """
+        Test :func:`which` when a command has been found.
+        """
+        # GIVEN: A mocked :func:`shutil.which` when the command is found.
+        with patch('openlp.core.lib.shutil.shutil.which',
+                   return_value=os.path.join('path', 'to', 'command')) as mocked_shutil_which:
+
+            # WHEN: Calling :func:`which` with a command that exists.
+            result = which('command')
+
+            # THEN: :func:`shutil.which` should have been called with the command, and :func:`which` should return a
+            #       Path object equivalent of the command path.
+            mocked_shutil_which.assert_called_once_with('command')
+            self.assertEqual(result, Path('path', 'to', 'command'))

=== modified file 'tests/functional/openlp_core_ui/test_exceptionform.py'
--- tests/functional/openlp_core_ui/test_exceptionform.py	2017-08-26 15:06:11 +0000
+++ tests/functional/openlp_core_ui/test_exceptionform.py	2017-09-18 20:44:53 +0000
@@ -103,7 +103,7 @@
             os.remove(self.tempfile)
 
     @patch("openlp.core.ui.exceptionform.Ui_ExceptionDialog")
-    @patch("openlp.core.ui.exceptionform.QtWidgets.QFileDialog")
+    @patch("openlp.core.ui.exceptionform.FileDialog")
     @patch("openlp.core.ui.exceptionform.QtCore.QUrl")
     @patch("openlp.core.ui.exceptionform.QtCore.QUrlQuery.addQueryItem")
     @patch("openlp.core.ui.exceptionform.Qt")

=== modified file 'tests/functional/openlp_plugins/presentations/test_impresscontroller.py'
--- tests/functional/openlp_plugins/presentations/test_impresscontroller.py	2017-06-08 21:36:17 +0000
+++ tests/functional/openlp_plugins/presentations/test_impresscontroller.py	2017-09-18 20:44:53 +0000
@@ -24,13 +24,12 @@
 """
 from unittest import TestCase
 from unittest.mock import MagicMock
-import os
 import shutil
 from tempfile import mkdtemp
 
 from openlp.core.common import Settings
-from openlp.plugins.presentations.lib.impresscontroller import \
-    ImpressController, ImpressDocument, TextType
+from openlp.core.common.path import Path
+from openlp.plugins.presentations.lib.impresscontroller import ImpressController, ImpressDocument, TextType
 from openlp.plugins.presentations.presentationplugin import __default_settings__
 
 from tests.utils.constants import TEST_RESOURCES_PATH
@@ -82,7 +81,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-09-18 20:44:53 +0000
@@ -26,6 +26,7 @@
 from unittest.mock import patch, MagicMock, call
 
 from openlp.core.common import Registry
+from openlp.core.common.path import Path
 from openlp.plugins.presentations.lib.mediaitem import PresentationMediaItem
 
 from tests.helpers.testmixin import TestMixin
@@ -92,17 +93,18 @@
         """
         # GIVEN: A mocked controller, and mocked os.path.getmtime
         mocked_controller = MagicMock()
-        mocked_doc = MagicMock()
+        mocked_doc = MagicMock(**{'get_thumbnail_path.return_value': Path()})
         mocked_controller.add_document.return_value = mocked_doc
         mocked_controller.supports = ['tmp']
         self.media_item.controllers = {
             'Mocked': mocked_controller
         }
-        presentation_file = '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]
-            mocked_exists.return_value = True
+
+        thmub_path = MagicMock(st_mtime=100)
+        file_path = MagicMock(st_mtime=400)
+        with patch.object(Path, 'stat', side_effect=[thmub_path, file_path]), \
+                patch.object(Path, 'exists', return_value=True):
+            presentation_file = Path('file.tmp')
 
             # WHEN: calling clean_up_thumbnails
             self.media_item.clean_up_thumbnails(presentation_file, True)
@@ -123,9 +125,8 @@
         self.media_item.controllers = {
             'Mocked': mocked_controller
         }
-        presentation_file = 'file.tmp'
-        with patch('openlp.plugins.presentations.lib.mediaitem.os.path.exists') as mocked_exists:
-            mocked_exists.return_value = False
+        presentation_file = Path('file.tmp')
+        with patch.object(Path, 'exists', return_value=False):
 
             # WHEN: calling clean_up_thumbnails
             self.media_item.clean_up_thumbnails(presentation_file, True)

=== 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-09-18 20:44:53 +0000
@@ -32,6 +32,7 @@
 
 from openlp.plugins.presentations.lib.pdfcontroller import PdfController, PdfDocument
 from openlp.core.common import Settings
+from openlp.core.common.path import Path
 from openlp.core.lib import ScreenList
 
 from tests.utils.constants import TEST_RESOURCES_PATH
@@ -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_pptviewcontroller.py'
--- tests/functional/openlp_plugins/presentations/test_pptviewcontroller.py	2017-05-30 18:42:35 +0000
+++ tests/functional/openlp_plugins/presentations/test_pptviewcontroller.py	2017-09-18 20:44:53 +0000
@@ -22,7 +22,6 @@
 """
 This module contains tests for the pptviewcontroller module of the Presentations plugin.
 """
-import os
 import shutil
 from tempfile import mkdtemp
 from unittest import TestCase
@@ -30,6 +29,7 @@
 
 from openlp.plugins.presentations.lib.pptviewcontroller import PptviewDocument, PptviewController
 from openlp.core.common import is_win
+from openlp.core.common.path import Path
 
 from tests.helpers.testmixin import TestMixin
 from tests.utils.constants import TEST_RESOURCES_PATH
@@ -184,7 +184,7 @@
         """
         # GIVEN: mocked PresentationController.save_titles_and_notes and a pptx file
         doc = PptviewDocument(self.mock_controller, self.mock_presentation)
-        doc.file_path = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'test.pptx')
+        doc.file_path = Path(TEST_RESOURCES_PATH, 'presentations', 'test.pptx')
         doc.save_titles_and_notes = MagicMock()
 
         # WHEN reading the titles and notes
@@ -201,13 +201,13 @@
         """
         # GIVEN: mocked PresentationController.save_titles_and_notes and an nonexistent file
         with patch('builtins.open') as mocked_open, \
-                patch('openlp.plugins.presentations.lib.pptviewcontroller.os.path.exists') as mocked_exists, \
+                patch.object(Path, 'exists') as mocked_path_exists, \
                 patch('openlp.plugins.presentations.lib.presentationcontroller.check_directory_exists') as \
                 mocked_dir_exists:
-            mocked_exists.return_value = False
+            mocked_path_exists.return_value = False
             mocked_dir_exists.return_value = False
             doc = PptviewDocument(self.mock_controller, self.mock_presentation)
-            doc.file_path = 'Idontexist.pptx'
+            doc.file_path = Path('Idontexist.pptx')
             doc.save_titles_and_notes = MagicMock()
 
             # WHEN: Reading the titles and notes
@@ -215,7 +215,7 @@
 
             # THEN: File existens should have been checked, and not have been opened.
             doc.save_titles_and_notes.assert_called_once_with(None, None)
-            mocked_exists.assert_any_call('Idontexist.pptx')
+            mocked_path_exists.assert_called_with()
             self.assertEqual(mocked_open.call_count, 0, 'There should be no calls to open a file.')
 
     def test_create_titles_and_notes_invalid_file(self):
@@ -228,7 +228,7 @@
             mocked_is_zf.return_value = False
             mocked_open.filesize = 10
             doc = PptviewDocument(self.mock_controller, self.mock_presentation)
-            doc.file_path = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'test.ppt')
+            doc.file_path = Path(TEST_RESOURCES_PATH, 'presentations', 'test.ppt')
             doc.save_titles_and_notes = MagicMock()
 
             # WHEN: reading the titles and notes

=== modified file 'tests/functional/openlp_plugins/presentations/test_presentationcontroller.py'
--- tests/functional/openlp_plugins/presentations/test_presentationcontroller.py	2017-08-25 20:03:25 +0000
+++ tests/functional/openlp_plugins/presentations/test_presentationcontroller.py	2017-09-18 20:44:53 +0000
@@ -23,9 +23,8 @@
 Functional tests to test the PresentationController and PresentationDocument
 classes and related methods.
 """
-import os
 from unittest import TestCase
-from unittest.mock import MagicMock, mock_open, patch
+from unittest.mock import MagicMock, call, patch
 
 from openlp.core.common.path import Path
 from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
@@ -67,23 +66,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):
         """
@@ -107,10 +101,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
@@ -121,45 +116,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()
@@ -180,18 +166,16 @@
             patch('openlp.plugins.presentations.lib.presentationcontroller.check_directory_exists')
         self.get_thumbnail_folder_patcher = \
             patch('openlp.plugins.presentations.lib.presentationcontroller.PresentationDocument.get_thumbnail_folder')
-        self.os_patcher = patch('openlp.plugins.presentations.lib.presentationcontroller.os')
         self._setup_patcher = \
             patch('openlp.plugins.presentations.lib.presentationcontroller.PresentationDocument._setup')
 
         self.mock_check_directory_exists = self.check_directory_exists_patcher.start()
         self.mock_get_thumbnail_folder = self.get_thumbnail_folder_patcher.start()
-        self.mock_os = self.os_patcher.start()
         self.mock_setup = self._setup_patcher.start()
 
         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):
         """
@@ -199,7 +183,6 @@
         """
         self.check_directory_exists_patcher.stop()
         self.get_thumbnail_folder_patcher.stop()
-        self.os_patcher.stop()
         self._setup_patcher.stop()
 
     def test_initialise_presentation_document(self):
@@ -227,7 +210,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(Path('returned', 'path'))
+        self.mock_check_directory_exists.assert_called_once_with(Path('returned', 'path/'))
 
         self._setup_patcher.start()
 
@@ -244,20 +227,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')


Follow ups