← Back to team overview

openlp-core team mailing list archive

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

 

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

Requested reviews:
  Tomas Groth (tomasgroth)
  OpenLP Core (openlp-core)
  Tim Bentley (trb143)
  Raoul Snyman (raoul-snyman)

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

Change the settings upgrade code to handle versioned upgrades
Upgrade settings to store file paths and json encoded Path objects
Enable the json encoders/decoders to work with custom objects with defined json methods


Add this to your merge proposal:
--------------------------------
lp:~phill-ridout/openlp/pathlib4 (revision 2767)
[SUCCESS] https://ci.openlp.io/job/Branch-01-Pull/2183/
[SUCCESS] https://ci.openlp.io/job/Branch-02-Functional-Tests/2086/
[SUCCESS] https://ci.openlp.io/job/Branch-03-Interface-Tests/1973/
[SUCCESS] https://ci.openlp.io/job/Branch-04a-Code_Analysis/1343/
[SUCCESS] https://ci.openlp.io/job/Branch-04b-Test_Coverage/1179/
[SUCCESS] https://ci.openlp.io/job/Branch-04c-Code_Analysis2/309/
[FAILURE] https://ci.openlp.io/job/Branch-05-AppVeyor-Tests/151/
Stopping after failure
-- 
Your team OpenLP Core is requested to review the proposed merge of lp:~phill-ridout/openlp/pathlib4 into lp:openlp.
=== modified file 'openlp/core/__init__.py'
--- openlp/core/__init__.py	2017-08-23 20:13:58 +0000
+++ openlp/core/__init__.py	2017-08-27 17:50:51 +0000
@@ -33,13 +33,13 @@
 import shutil
 import sys
 import time
-from pathlib import Path
 from traceback import format_exception
 
 from PyQt5 import QtCore, QtGui, QtWidgets
 
 from openlp.core.common import Registry, OpenLPMixin, AppLocation, LanguageManager, Settings, UiStrings, \
     check_directory_exists, is_macosx, is_win, translate
+from openlp.core.common.path import Path
 from openlp.core.common.versionchecker import VersionThread, get_application_version
 from openlp.core.lib import ScreenList
 from openlp.core.resources import qInitResources
@@ -347,8 +347,7 @@
     """
     Setup our logging using log_path
 
-    :param pathlib.Path log_path: The file to save the log to
-    :return: None
+    :param openlp.core.common.path.Path log_path: The file to save the log to.
     :rtype: None
     """
     check_directory_exists(log_path, True)
@@ -406,7 +405,7 @@
         # Set our data path
         log.info('Data path: {name}'.format(name=data_path))
         # Point to our data path
-        portable_settings.setValue('advanced/data path', str(data_path))
+        portable_settings.setValue('advanced/data path', data_path)
         portable_settings.setValue('advanced/is portable', True)
         portable_settings.sync()
     else:
@@ -423,8 +422,8 @@
     if application.is_data_path_missing():
         application.shared_memory.detach()
         sys.exit()
-    # Remove/convert obsolete settings.
-    Settings().remove_obsolete_settings()
+    # Upgrade settings.
+    Settings().upgrade_settings()
     # First time checks in settings
     if not Settings().value('core/has run wizard'):
         if not FirstTimeLanguageForm().exec():

=== modified file 'openlp/core/api/endpoint/controller.py'
--- openlp/core/api/endpoint/controller.py	2017-08-24 19:53:55 +0000
+++ openlp/core/api/endpoint/controller.py	2017-08-27 17:50:51 +0000
@@ -79,8 +79,7 @@
                     item['title'] = str(frame['display_title'])
                 if current_item.is_capable(ItemCapabilities.HasNotes):
                     item['slide_notes'] = str(frame['notes'])
-                if current_item.is_capable(ItemCapabilities.HasThumbnails) and \
-                        Settings().value('api/thumbnails'):
+                if current_item.is_capable(ItemCapabilities.HasThumbnails) and Settings().value('api/thumbnails'):
                     # If the file is under our app directory tree send the portion after the match
                     data_path = str(AppLocation.get_data_path())
                     if frame['image'][0:len(data_path)] == data_path:

=== modified file 'openlp/core/common/__init__.py'
--- openlp/core/common/__init__.py	2017-08-12 17:45:56 +0000
+++ openlp/core/common/__init__.py	2017-08-27 17:50:51 +0000
@@ -66,9 +66,8 @@
     """
     Check a directory exists and if not create it
 
-    :param pathlib.Path directory: The directory to make sure exists
+    :param openlp.core.common.path.Path directory: The directory to make sure exists
     :param bool do_not_log: To not log anything. This is need for the start up, when the log isn't ready.
-    :return: None
     :rtype: None
     """
     if not do_not_log:
@@ -89,7 +88,6 @@
     :param str glob_pattern: A glob pattern used to find the extension(s) to be imported. Should be relative to the
         application directory. i.e. plugins/*/*plugin.py
     :param list[str] excluded_files: A list of file names to exclude that the glob pattern may find.
-    :return: None
     :rtype: None
     """
     app_dir = AppLocation.get_directory(AppLocation.AppDir)
@@ -110,7 +108,7 @@
     """
     Convert a path to a module name (i.e openlp.core.common)
 
-    :param pathlib.Path path: The path to convert to a module name.
+    :param openlp.core.common.path.Path path: The path to convert to a module name.
     :return: The module name.
     :rtype: str
     """
@@ -377,7 +375,7 @@
     """
     Deletes a file from the system.
 
-    :param pathlib.Path file_path: The file, including path, to delete.
+    :param openlp.core.common.path.Path file_path: The file, including path, to delete.
     :return: True if the deletion was successful, or the file never existed. False otherwise.
     :rtype: bool
     """
@@ -412,7 +410,7 @@
     """
     Validate that the file is not an image file.
 
-    :param pathlib.Path file_path: The file to be checked.
+    :param openlp.core.common.path.Path file_path: The file to be checked.
     :return: If the file is not an image
     :rtype: bool
     """
@@ -440,7 +438,7 @@
     """
     Function that checks whether a binary exists.
 
-    :param pathlib.Path program_path: The full path to the binary to check.
+    :param openlp.core.common.path.Path program_path: The full path to the binary to check.
     :return: program output to be parsed
     :rtype: bytes
     """
@@ -466,7 +464,7 @@
     """
     Utility function to incrementally detect the file encoding.
 
-    :param pathlib.Path file_path: Filename for the file to determine the encoding for.
+    :param openlp.core.common.path.Path file_path: Filename for the file to determine the encoding for.
     :return: A dict with the keys 'encoding' and 'confidence'
     :rtype: dict[str, float]
     """

=== modified file 'openlp/core/common/applocation.py'
--- openlp/core/common/applocation.py	2017-08-18 19:34:20 +0000
+++ openlp/core/common/applocation.py	2017-08-27 17:50:51 +0000
@@ -25,9 +25,9 @@
 import logging
 import os
 import sys
-from pathlib import Path
 
 from openlp.core.common import Settings, is_win, is_macosx
+from openlp.core.common.path import Path
 
 
 if not is_win() and not is_macosx():
@@ -64,10 +64,8 @@
         Return the appropriate directory according to the directory type.
 
         :param dir_type: The directory type you want, for instance the data directory. Default *AppLocation.AppDir*
-        :type dir_type: AppLocation Enum
-
         :return: The requested path
-        :rtype: pathlib.Path
+        :rtype: openlp.core.common.path.Path
         """
         if dir_type == AppLocation.AppDir or dir_type == AppLocation.VersionDir:
             return get_frozen_path(FROZEN_APP_PATH, APP_PATH)
@@ -84,11 +82,11 @@
         Return the path OpenLP stores all its data under.
 
         :return: The data path to use.
-        :rtype: pathlib.Path
+        :rtype: openlp.core.common.path.Path
         """
         # Check if we have a different data location.
         if Settings().contains('advanced/data path'):
-            path = Path(Settings().value('advanced/data path'))
+            path = Settings().value('advanced/data path')
         else:
             path = AppLocation.get_directory(AppLocation.DataDir)
             check_directory_exists(path)
@@ -104,7 +102,7 @@
         :param str extension: Defaults to ''. The extension to search for. For example::
             '.png'
         :return: List of files found.
-        :rtype: list[pathlib.Path]
+        :rtype: list[openlp.core.common.path.Path]
         """
         path = AppLocation.get_data_path()
         if section:
@@ -120,9 +118,8 @@
         """
         Return the path a particular module stores its data under.
 
-        :type section: str
-
-        :rtype: pathlib.Path
+        :param str section:
+        :rtype: openlp.core.common.path.Path
         """
         path = AppLocation.get_data_path() / section
         check_directory_exists(path)
@@ -135,7 +132,7 @@
 
     :param dir_type: AppLocation Enum of the requested path type
     :return: The requested path
-    :rtype: pathlib.Path
+    :rtype: openlp.core.common.path.Path
     """
     # If running from source, return the language directory from the source directory
     if dir_type == AppLocation.LanguageDir:

=== added file 'openlp/core/common/json.py'
--- openlp/core/common/json.py	1970-01-01 00:00:00 +0000
+++ openlp/core/common/json.py	2017-08-27 17:50:51 +0000
@@ -0,0 +1,86 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2017 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+from json import JSONDecoder, JSONEncoder
+
+from openlp.core.common.path import Path
+
+
+class OpenLPJsonDecoder(JSONDecoder):
+    """
+    Implement a custom JSONDecoder to handle Path objects
+
+    Example Usage:
+        object = json.loads(json_string, cls=OpenLPJsonDecoder)
+    """
+    def __init__(self, object_hook=None, parse_float=None, parse_int=None, parse_constant=None, strict=True,
+                 object_pairs_hook=None, **kwargs):
+        """
+        Re-implement __init__ so that we can pass in our object_hook method. Any additional kwargs, are stored in the
+        instance and are passed to custom objects upon encoding or decoding.
+        """
+        self.kwargs = kwargs
+        if object_hook is None:
+            object_hook = self.custom_object_hook
+        super().__init__(object_hook, parse_float, parse_int, parse_constant, strict, object_pairs_hook)
+
+    def custom_object_hook(self, obj):
+        """
+        Implement a custom Path object decoder.
+
+        :param dict obj: A decoded JSON object
+        :return: The original object literal, or a Path object if the object literal contains a key '__Path__'
+        :rtype: dict | openlp.core.common.path.Path
+        """
+        if '__Path__' in obj:
+            obj = Path.encode_json(obj, **self.kwargs)
+        return obj
+
+
+class OpenLPJsonEncoder(JSONEncoder):
+    """
+    Implement a custom JSONEncoder to handle Path objects
+
+    Example Usage:
+        json_string = json.dumps(object, cls=OpenLPJsonEncoder)
+    """
+    def __init__(self, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, sort_keys=False,
+                 indent=None, separators=None, default=None, **kwargs):
+        """
+        Re-implement __init__ so that we can pass in additional kwargs, which are stored in the instance and are passed
+        to custom objects upon encoding or decoding.
+        """
+        self.kwargs = kwargs
+        if default is None:
+            default = self.custom_default
+        super().__init__(skipkeys, ensure_ascii, check_circular, allow_nan, sort_keys, indent, separators, default)
+
+    def custom_default(self, obj):
+        """
+        Convert any Path objects into a dictionary object which can be serialized.
+
+        :param object obj: The object to convert
+        :return: The serializable object
+        :rtype: dict
+        """
+        if isinstance(obj, Path):
+            return obj.json_object(**self.kwargs)
+        return super().default(obj)

=== modified file 'openlp/core/common/path.py'
--- openlp/core/common/path.py	2017-08-04 17:40:57 +0000
+++ openlp/core/common/path.py	2017-08-27 17:50:51 +0000
@@ -19,17 +19,21 @@
 # with this program; if not, write to the Free Software Foundation, Inc., 59  #
 # Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
 ###############################################################################
-
-from pathlib import Path
-
-
-def path_to_str(path):
+from contextlib import suppress
+
+from openlp.core.common import is_win
+
+if is_win():
+    from pathlib import WindowsPath as PathVariant
+else:
+    from pathlib import PosixPath as PathVariant
+
+
+def path_to_str(path=None):
     """
     A utility function to convert a Path object or NoneType to a string equivalent.
 
-    :param path: The value to convert to a string
-    :type: pathlib.Path or None
-
+    :param openlp.core.common.path.Path | None path: The value to convert to a string
     :return: An empty string if :param:`path` is None, else a string representation of the :param:`path`
     :rtype: str
     """
@@ -48,14 +52,49 @@
     This function is of particular use because initating a Path object with an empty string causes the Path object to
     point to the current working directory.
 
-    :param string: The string to convert
-    :type string: str
-
+    :param str string: The string to convert
     :return: None if :param:`string` is empty, or a Path object representation of :param:`string`
-    :rtype: pathlib.Path or None
+    :rtype: openlp.core.common.path.Path | None
     """
     if not isinstance(string, str):
         raise TypeError('parameter \'string\' must be of type str')
     if string == '':
         return None
     return Path(string)
+
+
+class Path(PathVariant):
+    """
+    Subclass pathlib.Path, so we can add json conversion methods
+    """
+    @staticmethod
+    def encode_json(obj, base_path=None, **kwargs):
+        """
+        Create a Path object from a dictionary representation. The dictionary has been constructed by JSON encoding of
+        a JSON reprensation of a Path object.
+
+        :param dict[str] obj: The dictionary representation
+        :param openlp.core.common.path.Path base_path: If specified, an absolute path to base the relative path off of.
+        :param kwargs: Contains any extra parameters. Not used!
+        :return: The reconstructed Path object
+        :rtype: openlp.core.common.path.Path
+        """
+        path = Path(*obj['__Path__'])
+        if base_path and not path.is_absolute():
+            return base_path / path
+        return path
+
+    def json_object(self, base_path=None, **kwargs):
+        """
+        Create a dictionary that can be JSON decoded.
+
+        :param openlp.core.common.path.Path base_path: If specified, an absolute path to make a relative path from.
+        :param kwargs: Contains any extra parameters. Not used!
+        :return: The dictionary representation of this Path object.
+        :rtype: dict[tuple]
+        """
+        path = self
+        if base_path:
+            with suppress(ValueError):
+                path = path.relative_to(base_path)
+        return {'__Path__': path.parts}

=== modified file 'openlp/core/common/settings.py'
--- openlp/core/common/settings.py	2017-08-25 04:26:25 +0000
+++ openlp/core/common/settings.py	2017-08-27 17:50:51 +0000
@@ -24,15 +24,18 @@
 """
 import datetime
 import logging
+import json
 import os
 
 from PyQt5 import QtCore, QtGui
 
-from openlp.core.common import ThemeLevel, SlideLimits, UiStrings, is_win, is_linux
-
+from openlp.core.common import SlideLimits, ThemeLevel, UiStrings, is_linux, is_win
+from openlp.core.common.json import OpenLPJsonDecoder, OpenLPJsonEncoder
+from openlp.core.common.path import Path, path_to_str, str_to_path
 
 log = logging.getLogger(__name__)
 
+__version__ = 2
 
 # Fix for bug #1014422.
 X11_BYPASS_DEFAULT = True
@@ -44,21 +47,6 @@
         X11_BYPASS_DEFAULT = False
 
 
-def recent_files_conv(value):
-    """
-    If the value is not a list convert it to a list
-    :param value: Value to convert
-    :return: value as a List
-    """
-    if isinstance(value, list):
-        return value
-    elif isinstance(value, str):
-        return [value]
-    elif isinstance(value, bytes):
-        return [value.decode()]
-    return []
-
-
 def media_players_conv(string):
     """
     If phonon is in the setting string replace it with system
@@ -73,14 +61,24 @@
     return string
 
 
+def file_names_conv(file_names):
+    """
+    Convert a list of file names in to a list of file paths.
+
+    :param list[str] file_names: The list of file names to convert.
+    :return: The list converted to file paths
+    :rtype: openlp.core.common.path.Path
+    """
+    return [str_to_path(file_name) for file_name in file_names]
+
+
 class Settings(QtCore.QSettings):
     """
     Class to wrap QSettings.
 
     * Exposes all the methods of QSettings.
-    * Adds functionality for OpenLP Portable. If the ``defaultFormat`` is set to
-      ``IniFormat``, and the path to the Ini file is set using ``set_filename``,
-      then the Settings constructor (without any arguments) will create a Settings
+    * Adds functionality for OpenLP Portable. If the ``defaultFormat`` is set to ``IniFormat``, and the path to the Ini
+      file is set using ``set_filename``, then the Settings constructor (without any arguments) will create a Settings
       object for accessing settings stored in that Ini file.
 
     ``__default_settings__``
@@ -91,7 +89,7 @@
 
             ('general/enable slide loop', 'advanced/slide limits', [(SlideLimits.Wrap, True), (SlideLimits.End, False)])
 
-        The first entry is the *old key*; it will be removed.
+        The first entry is the *old key*; if it is different from the *new key* it will be removed.
 
         The second entry is the *new key*; we will add it to the config. If this is just an empty string, we just remove
         the old key. The last entry is a list containing two-pair tuples. If the list is empty, no conversion is made.
@@ -105,11 +103,12 @@
         So, if the type of the old value is bool, then there must be two rules.
     """
     __default_settings__ = {
+        'settings/version': 0,
         'advanced/add page break': False,
         'advanced/alternate rows': not is_win(),
         'advanced/autoscrolling': {'dist': 1, 'pos': 0},
         'advanced/current media plugin': -1,
-        'advanced/data path': '',
+        'advanced/data path': None,
         # 7 stands for now, 0 to 6 is Monday to Sunday.
         'advanced/default service day': 7,
         'advanced/default service enabled': True,
@@ -143,7 +142,7 @@
         'api/authentication enabled': False,
         'api/ip address': '0.0.0.0',
         'api/thumbnails': True,
-        'crashreport/last directory': '',
+        'crashreport/last directory': None,
         'formattingTags/html_tags': '',
         'core/audio repeat list': False,
         'core/auto open': False,
@@ -162,7 +161,7 @@
         'core/screen blank': False,
         'core/show splash': True,
         'core/logo background color': '#ffffff',
-        'core/logo file': ':/graphics/openlp-splash-screen.png',
+        'core/logo file': Path(':/graphics/openlp-splash-screen.png'),
         'core/logo hide on startup': False,
         'core/songselect password': '',
         'core/songselect username': '',
@@ -177,17 +176,17 @@
         'media/players': 'system,webkit',
         'media/override player': QtCore.Qt.Unchecked,
         'players/background color': '#000000',
-        'servicemanager/last directory': '',
-        'servicemanager/last file': '',
-        'servicemanager/service theme': '',
+        'servicemanager/last directory': None,
+        'servicemanager/last file': None,
+        'servicemanager/service theme': None,
         'SettingsImport/file_date_created': datetime.datetime.now().strftime("%Y-%m-%d %H:%M"),
         'SettingsImport/Make_Changes': 'At_Own_RISK',
         'SettingsImport/type': 'OpenLP_settings_export',
         'SettingsImport/version': '',
         'themes/global theme': '',
-        'themes/last directory': '',
-        'themes/last directory export': '',
-        'themes/last directory import': '',
+        'themes/last directory': None,
+        'themes/last directory export': None,
+        'themes/last directory import': None,
         'themes/theme level': ThemeLevel.Song,
         'themes/wrap footer': False,
         'user interface/live panel': True,
@@ -208,22 +207,20 @@
         'projector/db database': '',
         'projector/enable': True,
         'projector/connect on start': False,
-        'projector/last directory import': '',
-        'projector/last directory export': '',
+        'projector/last directory import': None,
+        'projector/last directory export': None,
         'projector/poll time': 20,  # PJLink  timeout is 30 seconds
         'projector/socket timeout': 5,  # 5 second socket timeout
         'projector/source dialog type': 0  # Source select dialog box type
     }
     __file_path__ = ''
-    __obsolete_settings__ = [
+    __setting_upgrade_1__ = [
         # Changed during 2.2.x development.
-        # ('advanced/stylesheet fix', '', []),
-        # ('general/recent files', 'core/recent files', [(recent_files_conv, None)]),
         ('songs/search as type', 'advanced/search as type', []),
         ('media/players', 'media/players_temp', [(media_players_conv, None)]),  # Convert phonon to system
         ('media/players_temp', 'media/players', []),  # Move temp setting from above to correct setting
         ('advanced/default color', 'core/logo background color', []),  # Default image renamed + moved to general > 2.4.
-        ('advanced/default image', '/core/logo file', []),  # Default image renamed + moved to general after 2.4.
+        ('advanced/default image', 'core/logo file', []),  # Default image renamed + moved to general after 2.4.
         ('remotes/https enabled', '', []),
         ('remotes/https port', '', []),
         ('remotes/twelve hour', 'api/twelve hour', []),
@@ -234,7 +231,6 @@
         ('remotes/authentication enabled', 'api/authentication enabled', []),
         ('remotes/ip address', 'api/ip address', []),
         ('remotes/thumbnails', 'api/thumbnails', []),
-        ('advanced/default image', 'core/logo file', []),  # Default image renamed + moved to general after 2.4.
         ('shortcuts/escapeItem', 'shortcuts/desktopScreenEnable', []),  # Escape item was removed in 2.6.
         ('shortcuts/offlineHelpItem', 'shortcuts/userManualItem', []),  # Online and Offline help were combined in 2.6.
         ('shortcuts/onlineHelpItem', 'shortcuts/userManualItem', []),  # Online and Offline help were combined in 2.6.
@@ -243,7 +239,28 @@
         # Last search type was renamed to last used search type in 2.6 since Bible search value type changed in 2.6.
         ('songs/last search type', 'songs/last used search type', []),
         ('bibles/last search type', '', []),
-        ('custom/last search type', 'custom/last used search type', [])
+        ('custom/last search type', 'custom/last used search type', [])]
+
+    __setting_upgrade_2__ = [
+        # The following changes are being made for the conversion to using Path objects made in 2.6 development
+        ('advanced/data path', 'advanced/data path', [(str_to_path, None)]),
+        ('crashreport/last directory', 'crashreport/last directory', [(str_to_path, None)]),
+        ('servicemanager/last directory', 'servicemanager/last directory', [(str_to_path, None)]),
+        ('servicemanager/last file', 'servicemanager/last file', [(str_to_path, None)]),
+        ('themes/last directory', 'themes/last directory', [(str_to_path, None)]),
+        ('themes/last directory export', 'themes/last directory export', [(str_to_path, None)]),
+        ('themes/last directory import', 'themes/last directory import', [(str_to_path, None)]),
+        ('projector/last directory import', 'projector/last directory import', [(str_to_path, None)]),
+        ('projector/last directory export', 'projector/last directory export', [(str_to_path, None)]),
+        ('bibles/last directory import', 'bibles/last directory import', [(str_to_path, None)]),
+        ('presentations/pdf_program', 'presentations/pdf_program', [(str_to_path, None)]),
+        ('songs/last directory import', 'songs/last directory import', [(str_to_path, None)]),
+        ('songs/last directory export', 'songs/last directory export', [(str_to_path, None)]),
+        ('songusage/last directory export', 'songusage/last directory export', [(str_to_path, None)]),
+        ('core/recent files', 'core/recent files', [(file_names_conv, None)]),
+        ('media/media files', 'media/media files', [(file_names_conv, None)]),
+        ('presentations/presentations files', 'presentations/presentations files', [(file_names_conv, None)]),
+        ('core/logo file', 'core/logo file', [(str_to_path, None)])
     ]
 
     @staticmethod
@@ -256,13 +273,16 @@
         Settings.__default_settings__.update(default_values)
 
     @staticmethod
-    def set_filename(ini_file):
+    def set_filename(ini_path):
         """
         Sets the complete path to an Ini file to be used by Settings objects.
 
         Does not affect existing Settings objects.
+
+        :param openlp.core.common.path.Path ini_path: ini file path
+        :rtype: None
         """
-        Settings.__file_path__ = ini_file
+        Settings.__file_path__ = str(ini_path)
 
     @staticmethod
     def set_up_default_values():
@@ -431,14 +451,22 @@
             key = self.group() + '/' + key
         return Settings.__default_settings__[key]
 
-    def remove_obsolete_settings(self):
+    def upgrade_settings(self):
         """
         This method is only called to clean up the config. It removes old settings and it renames settings. See
         ``__obsolete_settings__`` for more details.
         """
-        for old_key, new_key, rules in Settings.__obsolete_settings__:
-            # Once removed we don't have to do this again.
-            if self.contains(old_key):
+        current_version = self.value('settings/version')
+        if __version__ == current_version:
+            return
+        for version in range(current_version, __version__):
+            version += 1
+            upgrade_list = getattr(self, '__setting_upgrade_{version}__'.format(version=version))
+            for old_key, new_key, rules in upgrade_list:
+                # Once removed we don't have to do this again. - Can be removed once fully switched to the versioning
+                # system.
+                if not self.contains(old_key):
+                    continue
                 if new_key:
                     # Get the value of the old_key.
                     old_value = super(Settings, self).value(old_key)
@@ -457,14 +485,17 @@
                             old_value = new
                             break
                     self.setValue(new_key, old_value)
-                self.remove(old_key)
+                if new_key != old_key:
+                    self.remove(old_key)
+        self.setValue('settings/version', version)
 
     def value(self, key):
         """
         Returns the value for the given ``key``. The returned ``value`` is of the same type as the default value in the
         *Settings.__default_settings__* dict.
 
-        :param key: The key to return the value from.
+        :param str key: The key to return the value from.
+        :return: The value stored by the setting.
         """
         # if group() is not empty the group has not been specified together with the key.
         if self.group():
@@ -474,6 +505,18 @@
         setting = super(Settings, self).value(key, default_value)
         return self._convert_value(setting, default_value)
 
+    def setValue(self, key, value):
+        """
+        Reimplement the setValue method to handle Path objects.
+
+        :param str key: The key of the setting to save
+        :param value: The value to save
+        :rtype: None
+        """
+        if isinstance(value, Path) or (isinstance(value, list) and value and isinstance(value[0], Path)):
+            value = json.dumps(value, cls=OpenLPJsonEncoder)
+        super().setValue(key, value)
+
     def _convert_value(self, setting, default_value):
         """
         This converts the given ``setting`` to the type of the given ``default_value``.
@@ -491,8 +534,11 @@
             if isinstance(default_value, str):
                 return ''
             # An empty list saved to the settings results in a None type being returned.
-            else:
+            elif isinstance(default_value, list):
                 return []
+        elif isinstance(setting, str):
+            if '__Path__' in setting:
+                return json.loads(setting, cls=OpenLPJsonDecoder)
         # Convert the setting to the correct type.
         if isinstance(default_value, bool):
             if isinstance(setting, bool):

=== modified file 'openlp/core/lib/__init__.py'
--- openlp/core/lib/__init__.py	2017-08-25 04:26:25 +0000
+++ openlp/core/lib/__init__.py	2017-08-27 17:50:51 +0000
@@ -89,7 +89,7 @@
     returns False. If there is an error loading the file or the content can't be decoded then the function will return
     None.
 
-    :param pathlib.Path text_file_path: The path to the file.
+    :param openlp.core.common.path.Path text_file_path: The path to the file.
     :return: The contents of the file, False if the file does not exist, or None if there is an Error reading or
     decoding the file.
     :rtype: str | False | None
@@ -610,17 +610,11 @@
     """
     Apply a transformation function to the specified args or kwargs
 
-    :param args: Positional arguments
-    :type args: (,)
-
-    :param kwargs: Key Word arguments
-    :type kwargs: dict
-
+    :param tuple args: Positional arguments
+    :param dict kwargs: Key Word arguments
     :param params: A tuple of tuples with the position and the key word to replace.
-    :type params: ((int, str, path_to_str),)
-
     :return: The modified positional and keyword arguments
-    :rtype: (tuple, dict)
+    :rtype: tuple[tuple, dict]
 
 
     Usage:

=== modified file 'openlp/core/lib/mediamanageritem.py'
--- openlp/core/lib/mediamanageritem.py	2017-08-11 20:47:52 +0000
+++ openlp/core/lib/mediamanageritem.py	2017-08-27 17:50:51 +0000
@@ -29,7 +29,7 @@
 from PyQt5 import QtCore, QtWidgets
 
 from openlp.core.common import Registry, RegistryProperties, Settings, UiStrings, translate
-from openlp.core.common.path import path_to_str, str_to_path
+from openlp.core.common.path import Path, path_to_str, str_to_path
 from openlp.core.lib import ServiceItem, StringContent, ServiceItemContext
 from openlp.core.lib.searchedit import SearchEdit
 from openlp.core.lib.ui import create_widget_action, critical_error_message_box
@@ -313,7 +313,7 @@
         """
         file_paths, selected_filter = FileDialog.getOpenFileNames(
             self, self.on_new_prompt,
-            str_to_path(Settings().value(self.settings_section + '/last directory')),
+            Settings().value(self.settings_section + '/last directory'),
             self.on_new_file_masks)
         log.info('New files(s) {file_paths}'.format(file_paths=file_paths))
         if file_paths:
@@ -377,9 +377,8 @@
                 self.list_view.clear()
             self.load_list(full_list, target_group)
             last_dir = os.path.split(files[0])[0]
-            Settings().setValue(self.settings_section + '/last directory', last_dir)
-            Settings().setValue('{section}/{section} files'.format(section=self.settings_section),
-                                self.get_file_list())
+            Settings().setValue(self.settings_section + '/last directory', Path(last_dir))
+            Settings().setValue('{section}/{section} files'.format(section=self.settings_section), self.get_file_list())
         if duplicates_found:
             critical_error_message_box(UiStrings().Duplicate,
                                        translate('OpenLP.MediaManagerItem',
@@ -400,13 +399,15 @@
     def get_file_list(self):
         """
         Return the current list of files
+
+        :rtype: list[openlp.core.common.path.Path]
         """
-        file_list = []
+        file_paths = []
         for index in range(self.list_view.count()):
             list_item = self.list_view.item(index)
             filename = list_item.data(QtCore.Qt.UserRole)
-            file_list.append(filename)
-        return file_list
+            file_paths.append(str_to_path(filename))
+        return file_paths
 
     def load_list(self, load_list, target_group):
         """

=== removed file 'openlp/core/lib/path.py'
--- openlp/core/lib/path.py	2017-08-02 06:09:38 +0000
+++ openlp/core/lib/path.py	1970-01-01 00:00:00 +0000
@@ -1,61 +0,0 @@
-# -*- coding: utf-8 -*-
-# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
-
-###############################################################################
-# OpenLP - Open Source Lyrics Projection                                      #
-# --------------------------------------------------------------------------- #
-# Copyright (c) 2008-2017 OpenLP Developers                                   #
-# --------------------------------------------------------------------------- #
-# This program is free software; you can redistribute it and/or modify it     #
-# under the terms of the GNU General Public License as published by the Free  #
-# Software Foundation; version 2 of the License.                              #
-#                                                                             #
-# This program is distributed in the hope that it will be useful, but WITHOUT #
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
-# more details.                                                               #
-#                                                                             #
-# You should have received a copy of the GNU General Public License along     #
-# with this program; if not, write to the Free Software Foundation, Inc., 59  #
-# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
-###############################################################################
-
-from pathlib import Path
-
-
-def path_to_str(path):
-    """
-    A utility function to convert a Path object or NoneType to a string equivalent.
-
-    :param path: The value to convert to a string
-    :type: pathlib.Path or None
-
-    :return: An empty string if :param:`path` is None, else a string representation of the :param:`path`
-    :rtype: str
-    """
-    if not isinstance(path, Path) and path is not None:
-        raise TypeError('parameter \'path\' must be of type Path or NoneType')
-    if path is None:
-        return ''
-    else:
-        return str(path)
-
-
-def str_to_path(string):
-    """
-    A utility function to convert a str object to a Path or NoneType.
-
-    This function is of particular use because initating a Path object with an empty string causes the Path object to
-    point to the current working directory.
-
-    :param string: The string to convert
-    :type string: str
-
-    :return: None if :param:`string` is empty, or a Path object representation of :param:`string`
-    :rtype: pathlib.Path or None
-    """
-    if not isinstance(string, str):
-        raise TypeError('parameter \'string\' must be of type str')
-    if string == '':
-        return None
-    return Path(string)

=== modified file 'openlp/core/lib/plugin.py'
--- openlp/core/lib/plugin.py	2016-12-31 11:01:36 +0000
+++ openlp/core/lib/plugin.py	2017-08-27 17:50:51 +0000
@@ -150,7 +150,7 @@
         self.status = PluginStatus.Inactive
         # Add the default status to the default settings.
         default_settings[name + '/status'] = PluginStatus.Inactive
-        default_settings[name + '/last directory'] = ''
+        default_settings[name + '/last directory'] = None
         # Append a setting for files in the mediamanager (note not all plugins
         # which have a mediamanager need this).
         if media_item_class is not None:

=== modified file 'openlp/core/ui/exceptionform.py'
--- openlp/core/ui/exceptionform.py	2016-12-31 11:01:36 +0000
+++ openlp/core/ui/exceptionform.py	2017-08-27 17:50:51 +0000
@@ -70,9 +70,9 @@
 except ImportError:
     VLC_VERSION = '-'
 
-from openlp.core.common import Settings, UiStrings, translate
+from openlp.core.common import RegistryProperties, Settings, UiStrings, is_linux, translate
 from openlp.core.common.versionchecker import get_application_version
-from openlp.core.common import RegistryProperties, is_linux
+from openlp.core.ui.lib.filedialog import FileDialog
 
 from .exceptiondialog import Ui_ExceptionDialog
 
@@ -139,17 +139,17 @@
         """
         Saving exception log and system information to a file.
         """
-        filename = QtWidgets.QFileDialog.getSaveFileName(
+        file_path, filter_used = FileDialog.getSaveFileName(
             self,
             translate('OpenLP.ExceptionForm', 'Save Crash Report'),
             Settings().value(self.settings_section + '/last directory'),
-            translate('OpenLP.ExceptionForm', 'Text files (*.txt *.log *.text)'))[0]
-        if filename:
-            filename = str(filename).replace('/', os.path.sep)
-            Settings().setValue(self.settings_section + '/last directory', os.path.dirname(filename))
+            translate('OpenLP.ExceptionForm', 'Text files (*.txt *.log *.text)'))
+        if file_path:
+            Settings().setValue(self.settings_section + '/last directory', file_path.parent)
             opts = self._create_report()
             report_text = self.report_text.format(version=opts['version'], description=opts['description'],
                                                   traceback=opts['traceback'], libs=opts['libs'], system=opts['system'])
+            filename = str(file_path)
             try:
                 report_file = open(filename, 'w')
                 try:
@@ -212,17 +212,16 @@
 
     def on_attach_file_button_clicked(self):
         """
-        Attache files to the bug report e-mail.
+        Attach files to the bug report e-mail.
         """
-        files, filter_used = QtWidgets.QFileDialog.getOpenFileName(self,
-                                                                   translate('ImagePlugin.ExceptionDialog',
-                                                                             'Select Attachment'),
-                                                                   Settings().value(self.settings_section +
-                                                                                    '/last directory'),
-                                                                   '{text} (*)'.format(text=UiStrings().AllFiles))
-        log.info('New files(s) {files}'.format(files=str(files)))
-        if files:
-            self.file_attachment = str(files)
+        file_path, filter_used = \
+            FileDialog.getOpenFileName(self,
+                                       translate('ImagePlugin.ExceptionDialog', 'Select Attachment'),
+                                       Settings().value(self.settings_section + '/last directory'),
+                                       '{text} (*)'.format(text=UiStrings().AllFiles))
+        log.info('New file {file}'.format(file=file_path))
+        if file_path:
+            self.file_attachment = str(file_path)
 
     def __button_state(self, state):
         """

=== modified file 'openlp/core/ui/firsttimeform.py'
--- openlp/core/ui/firsttimeform.py	2017-08-23 20:13:58 +0000
+++ openlp/core/ui/firsttimeform.py	2017-08-27 17:50:51 +0000
@@ -30,13 +30,13 @@
 import urllib.parse
 import urllib.error
 from configparser import ConfigParser, MissingSectionHeaderError, NoOptionError, NoSectionError
-from pathlib import Path
 from tempfile import gettempdir
 
 from PyQt5 import QtCore, QtWidgets
 
 from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, check_directory_exists, \
     translate, clean_button_text, trace_error_handler
+from openlp.core.common.path import Path
 from openlp.core.lib import PluginStatus, build_icon
 from openlp.core.lib.ui import critical_error_message_box
 from openlp.core.common.httputils import get_web_page, get_url_file_size, url_get_file, CONNECTION_TIMEOUT

=== modified file 'openlp/core/ui/generaltab.py'
--- openlp/core/ui/generaltab.py	2017-08-04 21:53:02 +0000
+++ openlp/core/ui/generaltab.py	2017-08-27 17:50:51 +0000
@@ -23,12 +23,11 @@
 The general tab of the configuration dialog.
 """
 import logging
-from pathlib import Path
 
 from PyQt5 import QtCore, QtGui, QtWidgets
 
 from openlp.core.common import Registry, Settings, UiStrings, translate, get_images_filter
-from openlp.core.common.path import path_to_str, str_to_path
+from openlp.core.common.path import Path, path_to_str, str_to_path
 from openlp.core.lib import SettingsTab, ScreenList
 from openlp.core.ui.lib import ColorButton, PathEdit
 
@@ -294,7 +293,7 @@
         self.auto_open_check_box.setChecked(settings.value('auto open'))
         self.show_splash_check_box.setChecked(settings.value('show splash'))
         self.logo_background_color = settings.value('logo background color')
-        self.logo_file_path_edit.path = str_to_path(settings.value('logo file'))
+        self.logo_file_path_edit.path = settings.value('logo file')
         self.logo_hide_on_startup_check_box.setChecked(settings.value('logo hide on startup'))
         self.logo_color_button.color = self.logo_background_color
         self.check_for_updates_check_box.setChecked(settings.value('update check'))
@@ -328,7 +327,7 @@
         settings.setValue('auto open', self.auto_open_check_box.isChecked())
         settings.setValue('show splash', self.show_splash_check_box.isChecked())
         settings.setValue('logo background color', self.logo_background_color)
-        settings.setValue('logo file', path_to_str(self.logo_file_path_edit.path))
+        settings.setValue('logo file', self.logo_file_path_edit.path)
         settings.setValue('logo hide on startup', self.logo_hide_on_startup_check_box.isChecked())
         settings.setValue('update check', self.check_for_updates_check_box.isChecked())
         settings.setValue('save prompt', self.save_check_service_check_box.isChecked())

=== modified file 'openlp/core/ui/lib/filedialog.py'
--- openlp/core/ui/lib/filedialog.py	2017-08-10 06:28:30 +0000
+++ openlp/core/ui/lib/filedialog.py	2017-08-27 17:50:51 +0000
@@ -20,11 +20,9 @@
 # Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
 ###############################################################################
 """ Patch the QFileDialog so it accepts and returns Path objects"""
-from pathlib import Path
-
 from PyQt5 import QtWidgets
 
-from openlp.core.common.path import path_to_str, str_to_path
+from openlp.core.common.path import Path, path_to_str, str_to_path
 from openlp.core.lib import replace_params
 
 
@@ -36,7 +34,7 @@
 
         :type parent: QtWidgets.QWidget or None
         :type caption: str
-        :type directory: pathlib.Path
+        :type directory: openlp.core.common.path.Path
         :type options: QtWidgets.QFileDialog.Options
         :rtype: tuple[Path, str]
         """
@@ -55,7 +53,7 @@
 
         :type parent: QtWidgets.QWidget or None
         :type caption: str
-        :type directory: pathlib.Path
+        :type directory: openlp.core.common.path.Path
         :type filter: str
         :type initialFilter: str
         :type options: QtWidgets.QFileDialog.Options
@@ -76,7 +74,7 @@
 
         :type parent: QtWidgets.QWidget or None
         :type caption: str
-        :type directory: pathlib.Path
+        :type directory: openlp.core.common.path.Path
         :type filter: str
         :type initialFilter: str
         :type options: QtWidgets.QFileDialog.Options
@@ -98,7 +96,7 @@
 
         :type parent: QtWidgets.QWidget or None
         :type caption: str
-        :type directory: pathlib.Path
+        :type directory: openlp.core.common.path.Path
         :type filter: str
         :type initialFilter: str
         :type options: QtWidgets.QFileDialog.Options

=== modified file 'openlp/core/ui/lib/pathedit.py'
--- openlp/core/ui/lib/pathedit.py	2017-08-07 20:50:01 +0000
+++ openlp/core/ui/lib/pathedit.py	2017-08-27 17:50:51 +0000
@@ -20,12 +20,11 @@
 # Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
 ###############################################################################
 from enum import Enum
-from pathlib import Path
 
 from PyQt5 import QtCore, QtWidgets
 
 from openlp.core.common import UiStrings, translate
-from openlp.core.common.path import path_to_str, str_to_path
+from openlp.core.common.path import Path, path_to_str, str_to_path
 from openlp.core.lib import build_icon
 from openlp.core.ui.lib.filedialog import FileDialog
 
@@ -46,19 +45,11 @@
         """
         Initialise the PathEdit widget
 
-        :param parent: The parent of the widget. This is just passed to the super method.
-        :type parent: QWidget or None
-
-        :param dialog_caption: Used to customise the caption in the QFileDialog.
-        :type dialog_caption: str
-
-        :param default_path: The default path. This is set as the path when the revert button is clicked
-        :type default_path: pathlib.Path
-
-        :param show_revert: Used to determine if the 'revert button' should be visible.
-        :type show_revert: bool
-
-        :return: None
+        :param QtWidget.QWidget | None: The parent of the widget. This is just passed to the super method.
+        :param str dialog_caption: Used to customise the caption in the QFileDialog.
+        :param openlp.core.common.path.Path default_path: The default path. This is set as the path when the revert
+            button is clicked
+        :param bool show_revert: Used to determine if the 'revert button' should be visible.
         :rtype: None
         """
         super().__init__(parent)
@@ -72,10 +63,7 @@
     def _setup(self, show_revert):
         """
         Set up the widget
-        :param show_revert: Show or hide the revert button
-        :type show_revert: bool
-
-        :return: None
+        :param bool show_revert: Show or hide the revert button
         :rtype: None
         """
         widget_layout = QtWidgets.QHBoxLayout()
@@ -102,7 +90,7 @@
         A property getter method to return the selected path.
 
         :return: The selected path
-        :rtype: pathlib.Path
+        :rtype: openlp.core.common.path.Path
         """
         return self._path
 
@@ -111,10 +99,7 @@
         """
         A Property setter method to set the selected path
 
-        :param path: The path to set the widget to
-        :type path: pathlib.Path
-
-        :return: None
+        :param openlp.core.common.path.Path path: The path to set the widget to
         :rtype: None
         """
         self._path = path
@@ -138,10 +123,7 @@
         """
         A Property setter method to set the path type
 
-        :param path_type: The type of path to select
-        :type path_type: PathType
-
-        :return: None
+        :param PathType path_type: The type of path to select
         :rtype: None
         """
         self._path_type = path_type
@@ -151,7 +133,6 @@
         """
         Called to update the tooltips on the buttons. This is changing path types, and when the widget is initalised
 
-        :return: None
         :rtype: None
         """
         if self._path_type == PathType.Directories:
@@ -167,7 +148,6 @@
 
         Show the QFileDialog and process the input from the user
 
-        :return: None
         :rtype: None
         """
         caption = self.dialog_caption
@@ -189,7 +169,6 @@
 
         Set the new path to the value of the default_path instance variable.
 
-        :return: None
         :rtype: None
         """
         self.on_new_path(self.default_path)
@@ -198,7 +177,6 @@
         """
         A handler to handle when the line edit has finished being edited.
 
-        :return: None
         :rtype: None
         """
         path = str_to_path(self.line_edit.text())
@@ -210,10 +188,7 @@
 
         Emits the pathChanged Signal
 
-        :param path: The new path
-        :type path: pathlib.Path
-
-        :return: None
+        :param openlp.core.common.path.Path path: The new path
         :rtype: None
         """
         if self._path != path:

=== modified file 'openlp/core/ui/lib/wizard.py'
--- openlp/core/ui/lib/wizard.py	2017-06-09 06:06:49 +0000
+++ openlp/core/ui/lib/wizard.py	2017-08-27 17:50:51 +0000
@@ -30,6 +30,7 @@
 from openlp.core.common import Registry, RegistryProperties, Settings, UiStrings, translate, is_macosx
 from openlp.core.lib import build_icon
 from openlp.core.lib.ui import add_welcome_page
+from openlp.core.ui.lib.filedialog import FileDialog
 
 log = logging.getLogger(__name__)
 
@@ -278,37 +279,38 @@
 
     def get_file_name(self, title, editbox, setting_name, filters=''):
         """
-        Opens a QFileDialog and saves the filename to the given editbox.
+        Opens a FileDialog and saves the filename to the given editbox.
 
-        :param title: The title of the dialog (unicode).
-        :param editbox:  An editbox (QLineEdit).
-        :param setting_name: The place where to save the last opened directory.
-        :param filters: The file extension filters. It should contain the file description
+        :param str title: The title of the dialog.
+        :param QtWidgets.QLineEdit editbox:  An QLineEdit.
+        :param str setting_name: The place where to save the last opened directory.
+        :param str filters: The file extension filters. It should contain the file description
             as well as the file extension. For example::
 
                 'OpenLP 2 Databases (*.sqlite)'
+        :rtype: None
         """
         if filters:
             filters += ';;'
         filters += '%s (*)' % UiStrings().AllFiles
-        filename, filter_used = QtWidgets.QFileDialog.getOpenFileName(
-            self, title, os.path.dirname(Settings().value(self.plugin.settings_section + '/' + setting_name)),
-            filters)
-        if filename:
-            editbox.setText(filename)
-        Settings().setValue(self.plugin.settings_section + '/' + setting_name, filename)
+        file_path, filter_used = FileDialog.getOpenFileName(
+            self, title, Settings().value(self.plugin.settings_section + '/' + setting_name), filters)
+        if file_path:
+            editbox.setText(str(file_path))
+            Settings().setValue(self.plugin.settings_section + '/' + setting_name, file_path.parent)
 
     def get_folder(self, title, editbox, setting_name):
         """
-        Opens a QFileDialog and saves the selected folder to the given editbox.
+        Opens a FileDialog and saves the selected folder to the given editbox.
 
-        :param title: The title of the dialog (unicode).
-        :param editbox: An editbox (QLineEdit).
-        :param setting_name: The place where to save the last opened directory.
+        :param str title: The title of the dialog.
+        :param QtWidgets.QLineEdit editbox: An QLineEditbox.
+        :param str setting_name: The place where to save the last opened directory.
+        :rtype: None
         """
-        folder = QtWidgets.QFileDialog.getExistingDirectory(
+        folder_path = FileDialog.getExistingDirectory(
             self, title, Settings().value(self.plugin.settings_section + '/' + setting_name),
             QtWidgets.QFileDialog.ShowDirsOnly)
-        if folder:
-            editbox.setText(folder)
-        Settings().setValue(self.plugin.settings_section + '/' + setting_name, folder)
+        if folder_path:
+            editbox.setText(str(folder_path))
+            Settings().setValue(self.plugin.settings_section + '/' + setting_name, folder_path)

=== modified file 'openlp/core/ui/maindisplay.py'
--- openlp/core/ui/maindisplay.py	2017-08-26 14:01:04 +0000
+++ openlp/core/ui/maindisplay.py	2017-08-27 17:50:51 +0000
@@ -37,6 +37,7 @@
 
 from openlp.core.common import AppLocation, Registry, RegistryProperties, OpenLPMixin, Settings, translate,\
     is_macosx, is_win
+from openlp.core.common.path import path_to_str
 from openlp.core.lib import ServiceItem, ImageSource, ScreenList, build_html, expand_tags, image_to_byte
 from openlp.core.lib.theme import BackgroundType
 from openlp.core.ui import HideMode, AlertLocation, DisplayControllerType
@@ -259,7 +260,7 @@
             background_color.setNamedColor(Settings().value('core/logo background color'))
             if not background_color.isValid():
                 background_color = QtCore.Qt.white
-            image_file = Settings().value('core/logo file')
+            image_file = path_to_str(Settings().value('core/logo file'))
             splash_image = QtGui.QImage(image_file)
             self.initial_fame = QtGui.QImage(
                 self.screen['size'].width(),

=== modified file 'openlp/core/ui/mainwindow.py'
--- openlp/core/ui/mainwindow.py	2017-08-23 20:13:58 +0000
+++ openlp/core/ui/mainwindow.py	2017-08-27 17:50:51 +0000
@@ -30,7 +30,6 @@
 from datetime import datetime
 from distutils import dir_util
 from distutils.errors import DistutilsFileError
-from pathlib import Path
 from tempfile import gettempdir
 
 from PyQt5 import QtCore, QtGui, QtWidgets
@@ -40,6 +39,7 @@
 from openlp.core.common import Registry, RegistryProperties, AppLocation, LanguageManager, Settings, UiStrings, \
     check_directory_exists, translate, is_win, is_macosx, add_actions
 from openlp.core.common.actions import ActionList, CategoryOrder
+from openlp.core.common.path import Path, path_to_str, str_to_path
 from openlp.core.common.versionchecker import get_application_version
 from openlp.core.lib import Renderer, PluginManager, ImageManager, PluginStatus, ScreenList, build_icon
 from openlp.core.lib.ui import create_action
@@ -879,8 +879,8 @@
         # Convert image files
         log.info('hook upgrade_plugin_settings')
         self.plugin_manager.hook_upgrade_plugin_settings(import_settings)
-        # Remove/rename old settings to prepare the import.
-        import_settings.remove_obsolete_settings()
+        # Upgrade settings to prepare the import.
+        import_settings.upgrade_settings()
         # Lets do a basic sanity check. If it contains this string we can assume it was created by OpenLP and so we'll
         # load what we can from it, and just silently ignore anything we don't recognise.
         if import_settings.value('SettingsImport/type') != 'OpenLP_settings_export':
@@ -1277,7 +1277,7 @@
         settings.remove('custom slide')
         settings.remove('service')
         settings.beginGroup(self.general_settings_section)
-        self.recent_files = settings.value('recent files')
+        self.recent_files = [path_to_str(file_path) for file_path in settings.value('recent files')]
         settings.endGroup()
         settings.beginGroup(self.ui_settings_section)
         self.move(settings.value('main window position'))
@@ -1301,7 +1301,7 @@
         log.debug('Saving QSettings')
         settings = Settings()
         settings.beginGroup(self.general_settings_section)
-        settings.setValue('recent files', self.recent_files)
+        settings.setValue('recent files', [str_to_path(file) for file in self.recent_files])
         settings.endGroup()
         settings.beginGroup(self.ui_settings_section)
         settings.setValue('main window position', self.pos())
@@ -1443,7 +1443,7 @@
             log.info('No data copy requested')
         # Change the location of data directory in config file.
         settings = QtCore.QSettings()
-        settings.setValue('advanced/data path', self.new_data_path)
+        settings.setValue('advanced/data path', Path(self.new_data_path))
         # Check if the new data path is our default.
         if self.new_data_path == str(AppLocation.get_directory(AppLocation.DataDir)):
             settings.remove('advanced/data path')

=== modified file 'openlp/core/ui/servicemanager.py'
--- openlp/core/ui/servicemanager.py	2017-08-23 20:13:58 +0000
+++ openlp/core/ui/servicemanager.py	2017-08-27 17:50:51 +0000
@@ -28,7 +28,6 @@
 import shutil
 import zipfile
 from datetime import datetime, timedelta
-from pathlib import Path
 from tempfile import mkstemp
 
 from PyQt5 import QtCore, QtGui, QtWidgets
@@ -36,11 +35,13 @@
 from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, ThemeLevel, OpenLPMixin, \
     RegistryMixin, check_directory_exists, UiStrings, translate, split_filename, delete_file
 from openlp.core.common.actions import ActionList, CategoryOrder
+from openlp.core.common.languagemanager import format_time
+from openlp.core.common.path import Path, path_to_str, str_to_path
 from openlp.core.lib import ServiceItem, ItemCapabilities, PluginStatus, build_icon
 from openlp.core.lib.ui import critical_error_message_box, create_widget_action, find_and_set_in_combo_box
 from openlp.core.ui import ServiceNoteForm, ServiceItemEditForm, StartTimeForm
 from openlp.core.ui.lib import OpenLPToolbar
-from openlp.core.common.languagemanager import format_time
+from openlp.core.ui.lib.filedialog import FileDialog
 
 
 class ServiceManagerList(QtWidgets.QTreeWidget):
@@ -373,7 +374,7 @@
         """
         self._file_name = str(file_name)
         self.main_window.set_service_modified(self.is_modified(), self.short_file_name())
-        Settings().setValue('servicemanager/last file', file_name)
+        Settings().setValue('servicemanager/last file', Path(file_name))
         self._save_lite = self._file_name.endswith('.oszl')
 
     def file_name(self):
@@ -435,18 +436,17 @@
             elif result == QtWidgets.QMessageBox.Save:
                 self.decide_save_method()
         if not load_file:
-            file_name, filter_used = QtWidgets.QFileDialog.getOpenFileName(
+            file_path, filter_used = FileDialog.getOpenFileName(
                 self.main_window,
                 translate('OpenLP.ServiceManager', 'Open File'),
                 Settings().value(self.main_window.service_manager_settings_section + '/last directory'),
                 translate('OpenLP.ServiceManager', 'OpenLP Service Files (*.osz *.oszl)'))
-            if not file_name:
+            if not file_path:
                 return False
         else:
-            file_name = load_file
-        Settings().setValue(self.main_window.service_manager_settings_section + '/last directory',
-                            split_filename(file_name)[0])
-        self.load_file(file_name)
+            file_path = str_to_path(load_file)
+        Settings().setValue(self.main_window.service_manager_settings_section + '/last directory', file_path.parent)
+        self.load_file(str(file_path))
 
     def save_modified_service(self):
         """
@@ -477,7 +477,7 @@
         self.set_file_name('')
         self.service_id += 1
         self.set_modified(False)
-        Settings().setValue('servicemanager/last file', '')
+        Settings().setValue('servicemanager/last file', None)
         self.plugin_manager.new_service_created()
 
     def create_basic_service(self):
@@ -513,7 +513,7 @@
         base_name = os.path.splitext(file_name)[0]
         service_file_name = '{name}.osj'.format(name=base_name)
         self.log_debug('ServiceManager.save_file - {name}'.format(name=path_file_name))
-        Settings().setValue(self.main_window.service_manager_settings_section + '/last directory', path)
+        Settings().setValue(self.main_window.service_manager_settings_section + '/last directory', Path(path))
         service = self.create_basic_service()
         write_list = []
         missing_list = []
@@ -634,7 +634,7 @@
         base_name = os.path.splitext(file_name)[0]
         service_file_name = '{name}.osj'.format(name=base_name)
         self.log_debug('ServiceManager.save_file - {name}'.format(name=path_file_name))
-        Settings().setValue(self.main_window.service_manager_settings_section + '/last directory', path)
+        Settings().setValue(self.main_window.service_manager_settings_section + '/last directory', Path(path))
         service = self.create_basic_service()
         self.application.set_busy_cursor()
         # Number of items + 1 to zip it
@@ -695,7 +695,7 @@
             default_file_name = format_time(default_pattern, local_time)
         else:
             default_file_name = ''
-        directory = Settings().value(self.main_window.service_manager_settings_section + '/last directory')
+        directory = path_to_str(Settings().value(self.main_window.service_manager_settings_section + '/last directory'))
         path = os.path.join(directory, default_file_name)
         # SaveAs from osz to oszl is not valid as the files will be deleted on exit which is not sensible or usable in
         # the long term.
@@ -778,7 +778,7 @@
                 delete_file(Path(p_file))
                 self.main_window.add_recent_file(file_name)
                 self.set_modified(False)
-                Settings().setValue('servicemanager/last file', file_name)
+                Settings().setValue('servicemanager/last file', Path(file_name))
             else:
                 critical_error_message_box(message=translate('OpenLP.ServiceManager', 'File is not a valid service.'))
                 self.log_error('File contains no service data')
@@ -843,7 +843,7 @@
         Load the last service item from the service manager when the service was last closed. Can be blank if there was
         no service present.
         """
-        file_name = Settings().value('servicemanager/last file')
+        file_name = str_to_path(Settings().value('servicemanager/last file'))
         if file_name:
             self.load_file(file_name)
 

=== modified file 'openlp/core/ui/themeform.py'
--- openlp/core/ui/themeform.py	2017-08-12 17:45:56 +0000
+++ openlp/core/ui/themeform.py	2017-08-27 17:50:51 +0000
@@ -24,12 +24,11 @@
 """
 import logging
 import os
-from pathlib import Path
 
 from PyQt5 import QtCore, QtGui, QtWidgets
 
 from openlp.core.common import Registry, RegistryProperties, UiStrings, translate, get_images_filter, is_not_image_file
-from openlp.core.common.path import path_to_str, str_to_path
+from openlp.core.common.path import Path, path_to_str, str_to_path
 from openlp.core.lib.theme import BackgroundType, BackgroundGradientType
 from openlp.core.lib.ui import critical_error_message_box
 from openlp.core.ui import ThemeLayoutForm

=== modified file 'openlp/core/ui/thememanager.py'
--- openlp/core/ui/thememanager.py	2017-08-12 17:45:56 +0000
+++ openlp/core/ui/thememanager.py	2017-08-27 17:50:51 +0000
@@ -25,14 +25,13 @@
 import os
 import zipfile
 import shutil
-from pathlib import Path
 
 from xml.etree.ElementTree import ElementTree, XML
 from PyQt5 import QtCore, QtGui, QtWidgets
 
 from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, OpenLPMixin, RegistryMixin, \
     UiStrings, check_directory_exists, translate, is_win, get_filesystem_encoding, delete_file
-from openlp.core.common.path import path_to_str, str_to_path
+from openlp.core.common.path import Path, path_to_str, str_to_path
 from openlp.core.lib import ImageSource, ValidationError, get_text_file_string, build_icon, \
     check_item_selected, create_thumb, validate_thumb
 from openlp.core.lib.theme import Theme, BackgroundType
@@ -379,16 +378,16 @@
             critical_error_message_box(message=translate('OpenLP.ThemeManager', 'You have not selected a theme.'))
             return
         theme = item.data(QtCore.Qt.UserRole)
-        path, filter_used = \
-            QtWidgets.QFileDialog.getSaveFileName(self.main_window,
-                                                  translate('OpenLP.ThemeManager', 'Save Theme - ({name})').
-                                                  format(name=theme),
-                                                  Settings().value(self.settings_section + '/last directory export'),
-                                                  translate('OpenLP.ThemeManager', 'OpenLP Themes (*.otz)'))
+        export_path, filter_used = \
+            FileDialog.getSaveFileName(self.main_window,
+                                       translate('OpenLP.ThemeManager', 'Save Theme - ({name})').
+                                       format(name=theme),
+                                       Settings().value(self.settings_section + '/last directory export'),
+                                       translate('OpenLP.ThemeManager', 'OpenLP Themes (*.otz)'))
         self.application.set_busy_cursor()
-        if path:
-            Settings().setValue(self.settings_section + '/last directory export', path)
-            if self._export_theme(path, theme):
+        if export_path:
+            Settings().setValue(self.settings_section + '/last directory export', export_path.parent)
+            if self._export_theme(str(export_path), theme):
                 QtWidgets.QMessageBox.information(self,
                                                   translate('OpenLP.ThemeManager', 'Theme Exported'),
                                                   translate('OpenLP.ThemeManager',
@@ -429,16 +428,15 @@
         file_paths, selected_filter = FileDialog.getOpenFileNames(
             self,
             translate('OpenLP.ThemeManager', 'Select Theme Import File'),
-            str_to_path(Settings().value(self.settings_section + '/last directory import')),
+            Settings().value(self.settings_section + '/last directory import'),
             translate('OpenLP.ThemeManager', 'OpenLP Themes (*.otz)'))
         self.log_info('New Themes {file_paths}'.format(file_paths=file_paths))
         if not file_paths:
             return
         self.application.set_busy_cursor()
         for file_path in file_paths:
-            file_name = path_to_str(file_path)
-            Settings().setValue(self.settings_section + '/last directory import', str(file_name))
-            self.unzip_theme(file_name, self.path)
+            self.unzip_theme(path_to_str(file_path), self.path)
+        Settings().setValue(self.settings_section + '/last directory import', file_path)
         self.load_themes()
         self.application.set_normal_cursor()
 

=== modified file 'openlp/core/ui/themewizard.py'
--- openlp/core/ui/themewizard.py	2017-08-07 20:50:01 +0000
+++ openlp/core/ui/themewizard.py	2017-08-27 17:50:51 +0000
@@ -22,11 +22,10 @@
 """
 The Create/Edit theme wizard
 """
-from pathlib import Path
-
 from PyQt5 import QtCore, QtGui, QtWidgets
 
 from openlp.core.common import UiStrings, translate, is_macosx
+from openlp.core.common.path import Path
 from openlp.core.lib import build_icon
 from openlp.core.lib.theme import HorizontalType, BackgroundType, BackgroundGradientType
 from openlp.core.lib.ui import add_welcome_page, create_valign_selection_widgets

=== modified file 'openlp/plugins/bibles/bibleplugin.py'
--- openlp/plugins/bibles/bibleplugin.py	2017-08-03 17:54:40 +0000
+++ openlp/plugins/bibles/bibleplugin.py	2017-08-27 17:50:51 +0000
@@ -59,7 +59,7 @@
     'bibles/range separator': '',
     'bibles/list separator': '',
     'bibles/end separator': '',
-    'bibles/last directory import': '',
+    'bibles/last directory import': None,
     'bibles/hide combined quick error': False,
     'bibles/is search while typing enabled': True
 }

=== modified file 'openlp/plugins/bibles/lib/importers/csvbible.py'
--- openlp/plugins/bibles/lib/importers/csvbible.py	2017-08-12 17:45:56 +0000
+++ openlp/plugins/bibles/lib/importers/csvbible.py	2017-08-27 17:50:51 +0000
@@ -51,9 +51,9 @@
 """
 import csv
 from collections import namedtuple
-from pathlib import Path
 
 from openlp.core.common import get_file_encoding, translate
+from openlp.core.common.path import Path
 from openlp.core.lib.exceptions import ValidationError
 from openlp.plugins.bibles.lib.bibleimport import BibleImport
 

=== modified file 'openlp/plugins/bibles/lib/manager.py'
--- openlp/plugins/bibles/lib/manager.py	2017-08-12 17:45:56 +0000
+++ openlp/plugins/bibles/lib/manager.py	2017-08-27 17:50:51 +0000
@@ -21,10 +21,9 @@
 ###############################################################################
 
 import logging
-import os
-from pathlib import Path
 
 from openlp.core.common import AppLocation, OpenLPMixin, RegistryProperties, Settings, translate, delete_file, UiStrings
+from openlp.core.common.path import Path
 from openlp.plugins.bibles.lib import LanguageSelection, parse_reference
 from openlp.plugins.bibles.lib.db import BibleDB, BibleMeta
 from .importers.csvbible import CSVBible
@@ -306,13 +305,10 @@
         """
         Does a verse search for the given bible and text.
 
-        :param bible: The bible to search
-        :type bible: str
-        :param text: The text to search for
-        :type text: str
-
+        :param str bible: The bible to search
+        :param str text: The text to search for
         :return: The search results if valid, or None if the search is invalid.
-        :rtype: None, list
+        :rtype: None | list
         """
         log.debug('BibleManager.verse_search("{bible}", "{text}")'.format(bible=bible, text=text))
         if not text:

=== modified file 'openlp/plugins/bibles/lib/mediaitem.py'
--- openlp/plugins/bibles/lib/mediaitem.py	2017-06-09 15:56:40 +0000
+++ openlp/plugins/bibles/lib/mediaitem.py	2017-08-27 17:50:51 +0000
@@ -465,8 +465,7 @@
         """
         Show the selected tab and set focus to it
 
-        :param index: The tab selected
-        :type index: int
+        :param int index: The tab selected
         :return: None
         """
         if index == SearchTabs.Search or index == SearchTabs.Select:
@@ -483,7 +482,7 @@
         Update list_widget with the contents of the selected list
 
         :param index: The index of the tab that has been changed to. (int)
-        :return: None
+        :rtype: None
         """
         if index == ResultsTab.Saved:
             self.add_built_results_to_list_widget(self.saved_results)

=== modified file 'openlp/plugins/images/lib/mediaitem.py'
--- openlp/plugins/images/lib/mediaitem.py	2017-08-23 20:13:58 +0000
+++ openlp/plugins/images/lib/mediaitem.py	2017-08-27 17:50:51 +0000
@@ -22,12 +22,12 @@
 
 import logging
 import os
-from pathlib import Path
 
 from PyQt5 import QtCore, QtGui, QtWidgets
 
 from openlp.core.common import Registry, AppLocation, Settings, UiStrings, check_directory_exists, translate, \
     delete_file, get_images_filter
+from openlp.core.common.path import Path
 from openlp.core.lib import ItemCapabilities, MediaManagerItem, ServiceItemContext, StringContent, build_icon, \
     check_item_selected, create_thumb, validate_thumb
 from openlp.core.lib.ui import create_widget_action, critical_error_message_box
@@ -390,7 +390,7 @@
         self.application.set_normal_cursor()
         self.load_list(files, target_group)
         last_dir = os.path.split(files[0])[0]
-        Settings().setValue(self.settings_section + '/last directory', last_dir)
+        Settings().setValue(self.settings_section + '/last directory', Path(last_dir))
 
     def load_list(self, images, target_group=None, initial_load=False):
         """

=== modified file 'openlp/plugins/media/lib/mediaitem.py'
--- openlp/plugins/media/lib/mediaitem.py	2017-08-23 20:13:58 +0000
+++ openlp/plugins/media/lib/mediaitem.py	2017-08-27 17:50:51 +0000
@@ -22,12 +22,12 @@
 
 import logging
 import os
-from pathlib import Path
 
 from PyQt5 import QtCore, QtWidgets
 
 from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, check_directory_exists, UiStrings,\
     translate
+from openlp.core.common.path import Path, path_to_str, str_to_path
 from openlp.core.lib import ItemCapabilities, MediaManagerItem, MediaType, ServiceItem, ServiceItemContext, \
     build_icon, check_item_selected
 from openlp.core.lib.ui import create_widget_action, critical_error_message_box, create_horizontal_adjusting_combo_box
@@ -303,7 +303,7 @@
         self.list_view.clear()
         self.service_path = os.path.join(str(AppLocation.get_section_data_path(self.settings_section)), 'thumbnails')
         check_directory_exists(Path(self.service_path))
-        self.load_list(Settings().value(self.settings_section + '/media files'))
+        self.load_list([path_to_str(file) for file in Settings().value(self.settings_section + '/media files')])
         self.rebuild_players()
 
     def rebuild_players(self):
@@ -401,14 +401,14 @@
         :param media_type: Type to get, defaults to audio.
         :return: The media list
         """
-        media = Settings().value(self.settings_section + '/media files')
-        media.sort(key=lambda filename: get_locale_key(os.path.split(str(filename))[1]))
+        media_file_paths = Settings().value(self.settings_section + '/media files')
+        media_file_paths.sort(key=lambda file_path: get_locale_key(file_path.name))
         if media_type == MediaType.Audio:
             extension = self.media_controller.audio_extensions_list
         else:
             extension = self.media_controller.video_extensions_list
         extension = [x[1:] for x in extension]
-        media = [x for x in media if os.path.splitext(x)[1] in extension]
+        media = [x for x in media_file_paths if x.suffix in extension]
         return media
 
     def search(self, string, show_error):
@@ -419,13 +419,12 @@
         :param show_error: Should the error be shown (True)
         :return: The search result.
         """
-        files = Settings().value(self.settings_section + '/media files')
         results = []
         string = string.lower()
-        for file in files:
-            filename = os.path.split(str(file))[1]
-            if filename.lower().find(string) > -1:
-                results.append([file, filename])
+        for file_path in Settings().value(self.settings_section + '/media files'):
+            file_name = file_path.name
+            if file_name.lower().find(string) > -1:
+                results.append([str(file_path), file_name])
         return results
 
     def on_load_optical(self):
@@ -446,13 +445,13 @@
 
         :param optical: The clip to add.
         """
-        full_list = self.get_file_list()
+        file_paths = self.get_file_list()
         # If the clip already is in the media list it isn't added and an error message is displayed.
-        if optical in full_list:
+        if optical in file_paths:
             critical_error_message_box(translate('MediaPlugin.MediaItem', 'Mediaclip already saved'),
                                        translate('MediaPlugin.MediaItem', 'This mediaclip has already been saved'))
             return
         # Append the optical string to the media list
-        full_list.append(optical)
+        file_paths.append(optical)
         self.load_list([optical])
-        Settings().setValue(self.settings_section + '/media files', self.get_file_list())
+        Settings().setValue(self.settings_section + '/media files', file_paths)

=== modified file 'openlp/plugins/media/mediaplugin.py'
--- openlp/plugins/media/mediaplugin.py	2017-08-23 20:13:58 +0000
+++ openlp/plugins/media/mediaplugin.py	2017-08-27 17:50:51 +0000
@@ -26,12 +26,11 @@
 import logging
 import os
 import re
-from pathlib import Path
-
 from PyQt5 import QtCore
 
 from openlp.core.api.http import register_endpoint
 from openlp.core.common import AppLocation, translate, check_binary_exists
+from openlp.core.common.path import Path
 from openlp.core.lib import Plugin, StringContent, build_icon
 from openlp.plugins.media.endpoint import api_media_endpoint, media_endpoint
 from openlp.plugins.media.lib import MediaMediaItem, MediaTab

=== modified file 'openlp/plugins/presentations/lib/impresscontroller.py'
--- openlp/plugins/presentations/lib/impresscontroller.py	2017-08-12 17:45:56 +0000
+++ openlp/plugins/presentations/lib/impresscontroller.py	2017-08-27 17:50:51 +0000
@@ -34,9 +34,9 @@
 import logging
 import os
 import time
-from pathlib import Path
 
-from openlp.core.common import is_win, Registry, get_uno_command, get_uno_instance, delete_file
+from openlp.core.common import is_win, Registry, delete_file
+from openlp.core.common.path import Path
 
 if is_win():
     from win32com.client import Dispatch

=== modified file 'openlp/plugins/presentations/lib/mediaitem.py'
--- openlp/plugins/presentations/lib/mediaitem.py	2017-03-03 19:27:31 +0000
+++ openlp/plugins/presentations/lib/mediaitem.py	2017-08-27 17:50:51 +0000
@@ -26,10 +26,11 @@
 from PyQt5 import QtCore, QtGui, QtWidgets
 
 from openlp.core.common import Registry, Settings, UiStrings, translate
+from openlp.core.common.languagemanager import get_locale_key
+from openlp.core.common.path import path_to_str
 from openlp.core.lib import MediaManagerItem, ItemCapabilities, ServiceItemContext,\
     build_icon, check_item_selected, create_thumb, validate_thumb
 from openlp.core.lib.ui import critical_error_message_box, create_horizontal_adjusting_combo_box
-from openlp.core.common.languagemanager import get_locale_key
 from openlp.plugins.presentations.lib import MessageListener
 from openlp.plugins.presentations.lib.pdfcontroller import PDF_CONTROLLER_FILETYPES
 
@@ -126,8 +127,8 @@
         Populate the media manager tab
         """
         self.list_view.setIconSize(QtCore.QSize(88, 50))
-        files = Settings().value(self.settings_section + '/presentations files')
-        self.load_list(files, initial_load=True)
+        file_paths = Settings().value(self.settings_section + '/presentations files')
+        self.load_list([path_to_str(file) for file in file_paths], initial_load=True)
         self.populate_display_types()
 
     def populate_display_types(self):
@@ -157,7 +158,7 @@
         existing files, and when the user adds new files via the media manager.
         """
         current_list = self.get_file_list()
-        titles = [os.path.split(file)[1] for file in current_list]
+        titles = [file_path.name for file_path in current_list]
         self.application.set_busy_cursor()
         if not initial_load:
             self.main_window.display_progress_bar(len(files))
@@ -410,11 +411,11 @@
         :param show_error: not used
         :return:
         """
-        files = Settings().value(self.settings_section + '/presentations files')
+        file_paths = Settings().value(self.settings_section + '/presentations files')
         results = []
         string = string.lower()
-        for file in files:
-            filename = os.path.split(str(file))[1]
-            if filename.lower().find(string) > -1:
-                results.append([file, filename])
+        for file_path in file_paths:
+            file_name = file_path.name
+            if file_name.lower().find(string) > -1:
+                results.append([path_to_str(file_path), file_name])
         return results

=== modified file 'openlp/plugins/presentations/lib/pdfcontroller.py'
--- openlp/plugins/presentations/lib/pdfcontroller.py	2017-08-12 17:45:56 +0000
+++ openlp/plugins/presentations/lib/pdfcontroller.py	2017-08-27 17:50:51 +0000
@@ -23,12 +23,12 @@
 import os
 import logging
 import re
-from pathlib import Path
 from shutil import which
 from subprocess import check_output, CalledProcessError
 
 from openlp.core.common import AppLocation, check_binary_exists
 from openlp.core.common import Settings, is_win
+from openlp.core.common.path import Path, path_to_str
 from openlp.core.lib import ScreenList
 from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
 
@@ -113,7 +113,7 @@
         self.also_supports = []
         # Use the user defined program if given
         if Settings().value('presentations/enable_pdf_program'):
-            pdf_program = Settings().value('presentations/pdf_program')
+            pdf_program = path_to_str(Settings().value('presentations/pdf_program'))
             program_type = self.process_check_binary(pdf_program)
             if program_type == 'gs':
                 self.gsbin = pdf_program

=== modified file 'openlp/plugins/presentations/lib/presentationcontroller.py'
--- openlp/plugins/presentations/lib/presentationcontroller.py	2017-08-12 17:45:56 +0000
+++ openlp/plugins/presentations/lib/presentationcontroller.py	2017-08-27 17:50:51 +0000
@@ -23,11 +23,11 @@
 import logging
 import os
 import shutil
-from pathlib import Path
 
 from PyQt5 import QtCore
 
 from openlp.core.common import Registry, AppLocation, Settings, check_directory_exists, md5_hash
+from openlp.core.common.path import Path
 from openlp.core.lib import create_thumb, validate_thumb
 
 log = logging.getLogger(__name__)

=== modified file 'openlp/plugins/presentations/lib/presentationtab.py'
--- openlp/plugins/presentations/lib/presentationtab.py	2017-08-04 21:53:02 +0000
+++ openlp/plugins/presentations/lib/presentationtab.py	2017-08-27 17:50:51 +0000
@@ -155,9 +155,7 @@
         enable_pdf_program = Settings().value(self.settings_section + '/enable_pdf_program')
         self.pdf_program_check_box.setChecked(enable_pdf_program)
         self.program_path_edit.setEnabled(enable_pdf_program)
-        pdf_program = Settings().value(self.settings_section + '/pdf_program')
-        if pdf_program:
-            self.program_path_edit.path = str_to_path(pdf_program)
+        self.program_path_edit.path = Settings().value(self.settings_section + '/pdf_program')
 
     def save(self):
         """
@@ -193,13 +191,13 @@
             Settings().setValue(setting_key, self.ppt_window_check_box.checkState())
             changed = True
         # Save pdf-settings
-        pdf_program = path_to_str(self.program_path_edit.path)
+        pdf_program_path = self.program_path_edit.path
         enable_pdf_program = self.pdf_program_check_box.checkState()
         # If the given program is blank disable using the program
-        if pdf_program == '':
+        if not pdf_program_path:
             enable_pdf_program = 0
-        if pdf_program != Settings().value(self.settings_section + '/pdf_program'):
-            Settings().setValue(self.settings_section + '/pdf_program', pdf_program)
+        if pdf_program_path != Settings().value(self.settings_section + '/pdf_program'):
+            Settings().setValue(self.settings_section + '/pdf_program', pdf_program_path)
             changed = True
         if enable_pdf_program != Settings().value(self.settings_section + '/enable_pdf_program'):
             Settings().setValue(self.settings_section + '/enable_pdf_program', enable_pdf_program)

=== modified file 'openlp/plugins/presentations/presentationplugin.py'
--- openlp/plugins/presentations/presentationplugin.py	2017-08-23 20:13:58 +0000
+++ openlp/plugins/presentations/presentationplugin.py	2017-08-27 17:50:51 +0000
@@ -39,7 +39,7 @@
 
 __default_settings__ = {'presentations/override app': QtCore.Qt.Unchecked,
                         'presentations/enable_pdf_program': QtCore.Qt.Unchecked,
-                        'presentations/pdf_program': '',
+                        'presentations/pdf_program': None,
                         'presentations/Impress': QtCore.Qt.Checked,
                         'presentations/Powerpoint': QtCore.Qt.Checked,
                         'presentations/Powerpoint Viewer': QtCore.Qt.Checked,

=== modified file 'openlp/plugins/songs/forms/editsongform.py'
--- openlp/plugins/songs/forms/editsongform.py	2017-08-23 20:13:58 +0000
+++ openlp/plugins/songs/forms/editsongform.py	2017-08-27 17:50:51 +0000
@@ -28,12 +28,11 @@
 import re
 import os
 import shutil
-from pathlib import Path
 
 from PyQt5 import QtCore, QtWidgets
 
 from openlp.core.common import Registry, RegistryProperties, AppLocation, UiStrings, check_directory_exists, translate
-from openlp.core.common.path import path_to_str
+from openlp.core.common.path import Path, path_to_str
 from openlp.core.lib import PluginStatus, MediaType, create_separated_list
 from openlp.core.lib.ui import set_case_insensitive_completer, critical_error_message_box, find_and_set_in_combo_box
 from openlp.core.ui.lib.filedialog import FileDialog

=== modified file 'openlp/plugins/songs/forms/songimportform.py'
--- openlp/plugins/songs/forms/songimportform.py	2017-08-07 20:50:01 +0000
+++ openlp/plugins/songs/forms/songimportform.py	2017-08-27 17:50:51 +0000
@@ -239,13 +239,11 @@
             filters += ';;'
         filters += '{text} (*)'.format(text=UiStrings().AllFiles)
         file_paths, selected_filter = FileDialog.getOpenFileNames(
-            self, title,
-            str_to_path(Settings().value(self.plugin.settings_section + '/last directory import')), filters)
+            self, title, Settings().value(self.plugin.settings_section + '/last directory import'), filters)
         if file_paths:
             file_names = [path_to_str(file_path) for file_path in file_paths]
             listbox.addItems(file_names)
-            Settings().setValue(self.plugin.settings_section + '/last directory import',
-                                os.path.split(str(file_names[0]))[0])
+            Settings().setValue(self.plugin.settings_section + '/last directory import', file_paths[0].parent)
 
     def get_list_of_files(self, list_box):
         """
@@ -363,14 +361,15 @@
     def on_error_save_to_button_clicked(self):
         """
         Save the error report to a file.
+
+        :rtype: None
         """
-        filename, filter_used = QtWidgets.QFileDialog.getSaveFileName(
+        file_path, filter_used = FileDialog.getSaveFileName(
             self, Settings().value(self.plugin.settings_section + '/last directory import'))
-        if not filename:
+        if not file_path:
             return
-        report_file = codecs.open(filename, 'w', 'utf-8')
-        report_file.write(self.error_report_text_edit.toPlainText())
-        report_file.close()
+        with file_path.open('w', encoding='utf-8') as report_file:
+            report_file.write(self.error_report_text_edit.toPlainText())
 
     def add_file_select_item(self):
         """

=== modified file 'openlp/plugins/songs/lib/importers/songbeamer.py'
--- openlp/plugins/songs/lib/importers/songbeamer.py	2017-08-12 17:45:56 +0000
+++ openlp/plugins/songs/lib/importers/songbeamer.py	2017-08-27 17:50:51 +0000
@@ -27,11 +27,11 @@
 import re
 import base64
 import math
-from pathlib import Path
 
+from openlp.core.common import Settings, is_win, is_macosx, get_file_encoding
+from openlp.core.common.path import Path
 from openlp.plugins.songs.lib import VerseType
 from openlp.plugins.songs.lib.importers.songimport import SongImport
-from openlp.core.common import Settings, is_win, is_macosx, get_file_encoding
 
 log = logging.getLogger(__name__)
 

=== modified file 'openlp/plugins/songs/lib/importers/songimport.py'
--- openlp/plugins/songs/lib/importers/songimport.py	2017-08-12 17:45:56 +0000
+++ openlp/plugins/songs/lib/importers/songimport.py	2017-08-27 17:50:51 +0000
@@ -24,11 +24,11 @@
 import re
 import shutil
 import os
-from pathlib import Path
 
 from PyQt5 import QtCore
 
 from openlp.core.common import Registry, AppLocation, check_directory_exists, translate
+from openlp.core.common.path import Path
 from openlp.core.ui.lib.wizard import WizardStrings
 from openlp.plugins.songs.lib import clean_song, VerseType
 from openlp.plugins.songs.lib.db import Song, Author, Topic, Book, MediaFile

=== modified file 'openlp/plugins/songs/lib/mediaitem.py'
--- openlp/plugins/songs/lib/mediaitem.py	2017-08-23 20:13:58 +0000
+++ openlp/plugins/songs/lib/mediaitem.py	2017-08-27 17:50:51 +0000
@@ -23,12 +23,12 @@
 import logging
 import os
 import shutil
-from pathlib import Path
 
 from PyQt5 import QtCore, QtWidgets
 from sqlalchemy.sql import and_, or_
 
 from openlp.core.common import Registry, AppLocation, Settings, check_directory_exists, UiStrings, translate
+from openlp.core.common.path import Path
 from openlp.core.lib import MediaManagerItem, ItemCapabilities, PluginStatus, ServiceItemContext, \
     check_item_selected, create_separated_list
 from openlp.core.lib.ui import create_widget_action

=== modified file 'openlp/plugins/songs/lib/openlyricsexport.py'
--- openlp/plugins/songs/lib/openlyricsexport.py	2017-08-12 17:45:56 +0000
+++ openlp/plugins/songs/lib/openlyricsexport.py	2017-08-27 17:50:51 +0000
@@ -25,11 +25,11 @@
 """
 import logging
 import os
-from pathlib import Path
 
 from lxml import etree
 
 from openlp.core.common import RegistryProperties, check_directory_exists, translate, clean_filename
+from openlp.core.common.path import Path
 from openlp.plugins.songs.lib.openlyricsxml import OpenLyrics
 
 log = logging.getLogger(__name__)

=== modified file 'openlp/plugins/songs/songsplugin.py'
--- openlp/plugins/songs/songsplugin.py	2017-06-11 19:41:34 +0000
+++ openlp/plugins/songs/songsplugin.py	2017-08-27 17:50:51 +0000
@@ -66,8 +66,8 @@
     'songs/display songbook': False,
     'songs/display written by': True,
     'songs/display copyright symbol': False,
-    'songs/last directory import': '',
-    'songs/last directory export': '',
+    'songs/last directory import': None,
+    'songs/last directory export': None,
     'songs/songselect username': '',
     'songs/songselect password': '',
     'songs/songselect searches': '',

=== modified file 'openlp/plugins/songusage/forms/songusagedetailform.py'
--- openlp/plugins/songusage/forms/songusagedetailform.py	2017-08-12 17:45:56 +0000
+++ openlp/plugins/songusage/forms/songusagedetailform.py	2017-08-27 17:50:51 +0000
@@ -22,13 +22,12 @@
 
 import logging
 import os
-from pathlib import Path
 
 from PyQt5 import QtCore, QtWidgets
 from sqlalchemy.sql import and_
 
 from openlp.core.common import RegistryProperties, Settings, check_directory_exists, translate
-from openlp.core.common.path import path_to_str, str_to_path
+from openlp.core.common.path import Path, path_to_str, str_to_path
 from openlp.core.lib.ui import critical_error_message_box
 from openlp.plugins.songusage.lib.db import SongUsageItem
 from .songusagedetaildialog import Ui_SongUsageDetailDialog
@@ -57,14 +56,16 @@
         """
         self.from_date_calendar.setSelectedDate(Settings().value(self.plugin.settings_section + '/from date'))
         self.to_date_calendar.setSelectedDate(Settings().value(self.plugin.settings_section + '/to date'))
-        self.report_path_edit.path = str_to_path(
-            Settings().value(self.plugin.settings_section + '/last directory export'))
+        self.report_path_edit.path = Settings().value(self.plugin.settings_section + '/last directory export')
 
     def on_report_path_edit_path_changed(self, file_path):
         """
-        Triggered when the Directory selection button is clicked
+        Called when the path in the `PathEdit` has changed
+
+        :param openlp.core.common.path.Path file_path: The new path.
+        :rtype: None
         """
-        Settings().setValue(self.plugin.settings_section + '/last directory export', path_to_str(file_path))
+        Settings().setValue(self.plugin.settings_section + '/last directory export', file_path)
 
     def accept(self):
         """

=== modified file 'openlp/plugins/songusage/songusageplugin.py'
--- openlp/plugins/songusage/songusageplugin.py	2016-12-31 11:01:36 +0000
+++ openlp/plugins/songusage/songusageplugin.py	2017-08-27 17:50:51 +0000
@@ -50,7 +50,7 @@
     'songusage/active': False,
     'songusage/to date': QtCore.QDate(YEAR, 8, 31),
     'songusage/from date': QtCore.QDate(YEAR - 1, 9, 1),
-    'songusage/last directory export': ''
+    'songusage/last directory export': None
 }
 
 

=== modified file 'tests/functional/openlp_core_common/test_applocation.py'
--- tests/functional/openlp_core_common/test_applocation.py	2017-08-12 17:45:56 +0000
+++ tests/functional/openlp_core_common/test_applocation.py	2017-08-27 17:50:51 +0000
@@ -22,13 +22,12 @@
 """
 Functional tests to test the AppLocation class and related methods.
 """
-import copy
 import os
-from pathlib import Path
 from unittest import TestCase
 from unittest.mock import MagicMock, patch
 
 from openlp.core.common import AppLocation, get_frozen_path
+from openlp.core.common.path import Path
 
 FILE_LIST = ['file1', 'file2', 'file3.txt', 'file4.txt', 'file5.mp3', 'file6.mp3']
 
@@ -43,12 +42,14 @@
         """
         with patch('openlp.core.common.applocation.Settings') as mocked_class, \
                 patch('openlp.core.common.AppLocation.get_directory') as mocked_get_directory, \
-                patch('openlp.core.common.applocation.check_directory_exists') as mocked_check_directory_exists:
+                patch('openlp.core.common.applocation.check_directory_exists') as mocked_check_directory_exists, \
+                patch('openlp.core.common.applocation.os') as mocked_os:
             # GIVEN: A mocked out Settings class and a mocked out AppLocation.get_directory()
             mocked_settings = mocked_class.return_value
             mocked_settings.contains.return_value = False
-            mocked_get_directory.return_value = Path('test', 'dir')
+            mocked_get_directory.return_value = os.path.join('test', 'dir')
             mocked_check_directory_exists.return_value = True
+            mocked_os.path.normpath.return_value = os.path.join('test', 'dir')
 
             # WHEN: we call AppLocation.get_data_path()
             data_path = AppLocation.get_data_path()
@@ -56,8 +57,8 @@
             # THEN: check that all the correct methods were called, and the result is correct
             mocked_settings.contains.assert_called_with('advanced/data path')
             mocked_get_directory.assert_called_with(AppLocation.DataDir)
-            mocked_check_directory_exists.assert_called_with(Path('test', 'dir'))
-            self.assertEqual(Path('test', 'dir'), data_path, 'Result should be "test/dir"')
+            mocked_check_directory_exists.assert_called_with(os.path.join('test', 'dir'))
+            self.assertEqual(os.path.join('test', 'dir'), data_path, 'Result should be "test/dir"')
 
     def test_get_data_path_with_custom_location(self):
         """

=== modified file 'tests/functional/openlp_core_common/test_common.py'
--- tests/functional/openlp_core_common/test_common.py	2017-08-12 17:45:56 +0000
+++ tests/functional/openlp_core_common/test_common.py	2017-08-27 17:50:51 +0000
@@ -22,13 +22,12 @@
 """
 Functional tests to test the AppLocation class and related methods.
 """
-from pathlib import Path
 from unittest import TestCase
 from unittest.mock import MagicMock, call, patch
 
-from openlp.core import common
 from openlp.core.common import check_directory_exists, clean_button_text, de_hump, extension_loader, is_macosx, \
     is_linux, is_win, path_to_module, trace_error_handler, translate
+from openlp.core.common.path import Path
 
 
 class TestCommonFunctions(TestCase):

=== modified file 'tests/functional/openlp_core_common/test_init.py'
--- tests/functional/openlp_core_common/test_init.py	2017-08-12 17:45:56 +0000
+++ tests/functional/openlp_core_common/test_init.py	2017-08-27 17:50:51 +0000
@@ -24,12 +24,12 @@
 """
 import os
 from io import BytesIO
-from pathlib import Path
 from unittest import TestCase
 from unittest.mock import MagicMock, PropertyMock, call, patch
 
 from openlp.core.common import add_actions, clean_filename, delete_file, get_file_encoding, get_filesystem_encoding,  \
     get_uno_command, get_uno_instance, split_filename
+from openlp.core.common.path import Path
 
 from tests.helpers.testmixin import TestMixin
 

=== added file 'tests/functional/openlp_core_common/test_json.py'
--- tests/functional/openlp_core_common/test_json.py	1970-01-01 00:00:00 +0000
+++ tests/functional/openlp_core_common/test_json.py	2017-08-27 17:50:51 +0000
@@ -0,0 +1,122 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2017 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+Package to test the openlp.core.common.json package.
+"""
+import json
+from unittest import TestCase
+from unittest.mock import patch
+
+from openlp.core.common.path import Path
+from openlp.core.common.json import OpenLPJsonDecoder, OpenLPJsonEncoder
+
+
+class TestOpenLPJsonDecoder(TestCase):
+    """
+    Test the OpenLPJsonDecoder class
+    """
+    def test_object_hook_path_object(self):
+        """
+        Test the object_hook method when called with a decoded Path JSON object
+        """
+        # GIVEN: An instance of OpenLPJsonDecoder
+        instance = OpenLPJsonDecoder()
+
+        # WHEN: Calling the object_hook method with a decoded JSON object which contains a Path
+        result = instance.object_hook({'__Path__': ['test', 'path']})
+
+        # THEN: A Path object should be returned
+        self.assertEqual(result, Path('test', 'path'))
+
+    def test_object_hook_non_path_object(self):
+        """
+        Test the object_hook method when called with a decoded JSON object
+        """
+        # GIVEN: An instance of OpenLPJsonDecoder
+        instance = OpenLPJsonDecoder()
+
+        # WHEN: Calling the object_hook method with a decoded JSON object which contains a Path
+        with patch('openlp.core.common.json.Path') as mocked_path:
+            result = instance.object_hook({'key': 'value'})
+
+            # THEN: The object should be returned unchanged and a Path object should not have been initiated
+            self.assertEqual(result, {'key': 'value'})
+            self.assertFalse(mocked_path.called)
+
+    def test_json_decode(self):
+        """
+        Test the OpenLPJsonDecoder when decoding a JSON string
+        """
+        # GIVEN: A JSON encoded string
+        json_string = '[{"__Path__": ["test", "path1"]}, {"__Path__": ["test", "path2"]}]'
+
+        # WHEN: Decoding the string using the OpenLPJsonDecoder class
+        obj = json.loads(json_string, cls=OpenLPJsonDecoder)
+
+        # THEN: The object returned should be a python version of the JSON string
+        self.assertEqual(obj, [Path('test', 'path1'), Path('test', 'path2')])
+
+
+class TestOpenLPJsonEncoder(TestCase):
+    """
+    Test the OpenLPJsonEncoder class
+    """
+    def test_default_path_object(self):
+        """
+        Test the default method when called with a Path object
+        """
+        # GIVEN: An instance of OpenLPJsonEncoder
+        instance = OpenLPJsonEncoder()
+
+        # WHEN: Calling the default method with a Path object
+        result = instance.default(Path('test', 'path'))
+
+        # THEN: A dictionary object that can be JSON encoded should be returned
+        self.assertEqual(result, {'__Path__': ('test', 'path')})
+
+    def test_default_non_path_object(self):
+        """
+        Test the default method when called with a object other than a Path object
+        """
+        with patch('openlp.core.common.json.JSONEncoder.default') as mocked_super_default:
+
+            # GIVEN: An instance of OpenLPJsonEncoder
+            instance = OpenLPJsonEncoder()
+
+            # WHEN: Calling the default method with a object other than a Path object
+            instance.default('invalid object')
+
+            # THEN: default method of the super class should have been called
+            mocked_super_default.assert_called_once_with('invalid object')
+
+    def test_json_encode(self):
+        """
+        Test the OpenLPJsonDEncoder when encoding an object conatining Path objects
+        """
+        # GIVEN: A list of Path objects
+        obj = [Path('test', 'path1'), Path('test', 'path2')]
+
+        # WHEN: Encoding the object using the OpenLPJsonEncoder class
+        json_string = json.dumps(obj, cls=OpenLPJsonEncoder)
+
+        # THEN: The JSON string return should be a representation of the object encoded
+        self.assertEqual(json_string, '[{"__Path__": ["test", "path1"]}, {"__Path__": ["test", "path2"]}]')

=== modified file 'tests/functional/openlp_core_common/test_path.py'
--- tests/functional/openlp_core_common/test_path.py	2017-08-04 18:06:43 +0000
+++ tests/functional/openlp_core_common/test_path.py	2017-08-27 17:50:51 +0000
@@ -23,10 +23,9 @@
 Package to test the openlp.core.common.path package.
 """
 import os
-from pathlib import Path
 from unittest import TestCase
 
-from openlp.core.common.path import path_to_str, str_to_path
+from openlp.core.common.path import Path, path_to_str, str_to_path
 
 
 class TestPath(TestCase):
@@ -86,3 +85,54 @@
 
         # THEN: `path_to_str` should return None
         self.assertEqual(result, None)
+
+    def test_path_encode_json(self):
+        """
+        Test that `Path.encode_json` returns a Path object from a dictionary representation of a Path object decoded
+        from JSON
+        """
+        # GIVEN: A Path object from openlp.core.common.path
+        # WHEN: Calling encode_json, with a dictionary representation
+        path = Path.encode_json({'__Path__': ['path', 'to', 'fi.le']}, extra=1, args=2)
+
+        # THEN: A Path object should have been returned
+        self.assertEqual(path, Path('path', 'to', 'fi.le'))
+
+    def test_path_encode_json_base_path(self):
+        """
+        Test that `Path.encode_json` returns a Path object from a dictionary representation of a Path object decoded
+        from JSON when the base_path arg is supplied.
+        """
+        # GIVEN: A Path object from openlp.core.common.path
+        # WHEN: Calling encode_json, with a dictionary representation
+        path = Path.encode_json({'__Path__': ['path', 'to', 'fi.le']}, base_path=Path('/base'))
+
+        # THEN: A Path object should have been returned with an absolute path
+        self.assertEqual(path, Path('/', 'base', 'path', 'to', 'fi.le'))
+
+    def test_path_json_object(self):
+        """
+        Test that `Path.json_object` creates a JSON decode-able object from a Path object
+        """
+        # GIVEN: A Path object from openlp.core.common.path
+        path = Path('/base', 'path', 'to', 'fi.le')
+
+        # WHEN: Calling json_object
+        obj = path.json_object(extra=1, args=2)
+
+        # THEN: A JSON decodable object should have been returned.
+        self.assertEqual(obj, {'__Path__': ('/', 'base', 'path', 'to', 'fi.le')})
+
+    def test_path_json_object_base_path(self):
+        """
+        Test that `Path.json_object` creates a JSON decode-able object from a Path object, that is relative to the
+        base_path
+        """
+        # GIVEN: A Path object from openlp.core.common.path
+        path = Path('/base', 'path', 'to', 'fi.le')
+
+        # WHEN: Calling json_object with a base_path
+        obj = path.json_object(base_path=Path('/', 'base'))
+
+        # THEN: A JSON decodable object should have been returned.
+        self.assertEqual(obj, {'__Path__': ('path', 'to', 'fi.le')})

=== modified file 'tests/functional/openlp_core_common/test_settings.py'
--- tests/functional/openlp_core_common/test_settings.py	2017-04-24 05:17:55 +0000
+++ tests/functional/openlp_core_common/test_settings.py	2017-08-27 17:50:51 +0000
@@ -26,7 +26,6 @@
 from unittest.mock import patch
 
 from openlp.core.common import Settings
-from openlp.core.common.settings import recent_files_conv
 
 from tests.helpers.testmixin import TestMixin
 
@@ -48,25 +47,6 @@
         """
         self.destroy_settings()
 
-    def test_recent_files_conv(self):
-        """
-        Test that recent_files_conv, converts various possible types of values correctly.
-        """
-        # GIVEN: A list of possible value types and the expected results
-        possible_values = [(['multiple', 'values'], ['multiple', 'values']),
-                           (['single value'], ['single value']),
-                           ('string value', ['string value']),
-                           (b'bytes value', ['bytes value']),
-                           ([], []),
-                           (None, [])]
-
-        # WHEN: Calling recent_files_conv with the possible values
-        for value, expected_result in possible_values:
-            actual_result = recent_files_conv(value)
-
-            # THEN: The actual result should be the same as the expected result
-            self.assertEqual(actual_result, expected_result)
-
     def test_settings_basic(self):
         """
         Test the Settings creation and its default usage

=== modified file 'tests/functional/openlp_core_lib/test_db.py'
--- tests/functional/openlp_core_lib/test_db.py	2017-08-12 17:45:56 +0000
+++ tests/functional/openlp_core_lib/test_db.py	2017-08-27 17:50:51 +0000
@@ -22,9 +22,7 @@
 """
 Package to test the openlp.core.lib package.
 """
-import os
 import shutil
-from pathlib import Path
 
 from tempfile import mkdtemp
 from unittest import TestCase
@@ -34,6 +32,7 @@
 from sqlalchemy.orm.scoping import ScopedSession
 from sqlalchemy import MetaData
 
+from openlp.core.common.path import Path
 from openlp.core.lib.db import init_db, get_upgrade_op, delete_database, upgrade_db
 from openlp.core.lib.projector import upgrade as pjlink_upgrade
 

=== modified file 'tests/functional/openlp_core_lib/test_lib.py'
--- tests/functional/openlp_core_lib/test_lib.py	2017-08-25 04:26:25 +0000
+++ tests/functional/openlp_core_lib/test_lib.py	2017-08-27 17:50:51 +0000
@@ -24,12 +24,12 @@
 """
 import os
 from datetime import datetime, timedelta
-from pathlib import Path
 from unittest import TestCase
 from unittest.mock import MagicMock, patch
 
 from PyQt5 import QtCore, QtGui
 
+from openlp.core.common.path import Path
 from openlp.core.lib import FormattingTags, build_icon, check_item_selected, clean_tags, compare_chord_lyric, \
     create_separated_list, create_thumb, expand_chords, expand_chords_for_printing, expand_tags, find_formatting_tags, \
     get_text_file_string, image_to_byte, replace_params, resize_image, str_to_bool, validate_thumb

=== modified file 'tests/functional/openlp_core_lib/test_path.py'
--- tests/functional/openlp_core_lib/test_path.py	2017-08-02 06:09:38 +0000
+++ tests/functional/openlp_core_lib/test_path.py	2017-08-27 17:50:51 +0000
@@ -23,10 +23,9 @@
 Package to test the openlp.core.lib.path package.
 """
 import os
-from pathlib import Path
 from unittest import TestCase
 
-from openlp.core.lib.path import path_to_str, str_to_path
+from openlp.core.common.path import Path, path_to_str, str_to_path
 
 
 class TestPath(TestCase):

=== modified file 'tests/functional/openlp_core_ui/test_exceptionform.py'
--- tests/functional/openlp_core_ui/test_exceptionform.py	2017-04-24 05:17:55 +0000
+++ tests/functional/openlp_core_ui/test_exceptionform.py	2017-08-27 17:50:51 +0000
@@ -29,6 +29,7 @@
 from unittest.mock import mock_open, patch
 
 from openlp.core.common import Registry
+from openlp.core.common.path import Path
 from openlp.core.ui import exceptionform
 
 from tests.helpers.testmixin import TestMixin
@@ -154,7 +155,7 @@
         # THEN: Verify strings were formatted properly
         mocked_add_query_item.assert_called_with('body', MAIL_ITEM_TEXT)
 
-    @patch("openlp.core.ui.exceptionform.QtWidgets.QFileDialog.getSaveFileName")
+    @patch("openlp.core.ui.exceptionform.FileDialog.getSaveFileName")
     @patch("openlp.core.ui.exceptionform.Qt")
     def test_on_save_report_button_clicked(self,
                                            mocked_qt,
@@ -181,7 +182,7 @@
         mocked_qt.PYQT_VERSION_STR = 'PyQt5 Test'
         mocked_is_linux.return_value = False
         mocked_application_version.return_value = 'Trunk Test'
-        mocked_save_filename.return_value = ['testfile.txt', ]
+        mocked_save_filename.return_value = (Path('testfile.txt'), 'filter')
 
         test_form = exceptionform.ExceptionForm()
         test_form.file_attachment = None

=== modified file 'tests/functional/openlp_core_ui/test_firsttimeform.py'
--- tests/functional/openlp_core_ui/test_firsttimeform.py	2017-08-12 17:45:56 +0000
+++ tests/functional/openlp_core_ui/test_firsttimeform.py	2017-08-27 17:50:51 +0000
@@ -25,11 +25,11 @@
 import os
 import tempfile
 import urllib
-from pathlib import Path
 from unittest import TestCase
 from unittest.mock import MagicMock, patch
 
 from openlp.core.common import Registry
+from openlp.core.common.path import Path
 from openlp.core.ui.firsttimeform import FirstTimeForm
 
 from tests.helpers.testmixin import TestMixin

=== modified file 'tests/functional/openlp_core_ui/test_themeform.py'
--- tests/functional/openlp_core_ui/test_themeform.py	2017-08-04 17:40:57 +0000
+++ tests/functional/openlp_core_ui/test_themeform.py	2017-08-27 17:50:51 +0000
@@ -22,10 +22,10 @@
 """
 Package to test the openlp.core.ui.themeform package.
 """
-from pathlib import Path
 from unittest import TestCase
 from unittest.mock import MagicMock, patch
 
+from openlp.core.common.path import Path
 from openlp.core.ui import ThemeForm
 
 

=== modified file 'tests/functional/openlp_core_ui_lib/test_filedialog.py'
--- tests/functional/openlp_core_ui_lib/test_filedialog.py	2017-08-07 21:01:16 +0000
+++ tests/functional/openlp_core_ui_lib/test_filedialog.py	2017-08-27 17:50:51 +0000
@@ -1,10 +1,10 @@
 import os
 from unittest import TestCase
 from unittest.mock import patch
-from pathlib import Path
 
 from PyQt5 import QtWidgets
 
+from openlp.core.common.path import Path
 from openlp.core.ui.lib.filedialog import FileDialog
 
 

=== modified file 'tests/functional/openlp_core_ui_lib/test_pathedit.py'
--- tests/functional/openlp_core_ui_lib/test_pathedit.py	2017-08-07 20:50:01 +0000
+++ tests/functional/openlp_core_ui_lib/test_pathedit.py	2017-08-27 17:50:51 +0000
@@ -23,10 +23,10 @@
 This module contains tests for the openlp.core.ui.lib.pathedit module
 """
 import os
-from pathlib import Path
 from unittest import TestCase
 from unittest.mock import MagicMock, PropertyMock, patch
 
+from openlp.core.common.path import Path
 from openlp.core.ui.lib import PathEdit, PathType
 from openlp.core.ui.lib.filedialog import FileDialog
 

=== modified file 'tests/functional/openlp_plugins/bibles/test_manager.py'
--- tests/functional/openlp_plugins/bibles/test_manager.py	2017-08-12 17:45:56 +0000
+++ tests/functional/openlp_plugins/bibles/test_manager.py	2017-08-27 17:50:51 +0000
@@ -22,10 +22,10 @@
 """
 This module contains tests for the manager submodule of the Bibles plugin.
 """
-from pathlib import Path
 from unittest import TestCase
 from unittest.mock import MagicMock, patch
 
+from openlp.core.common.path import Path
 from openlp.plugins.bibles.lib.manager import BibleManager
 
 

=== modified file 'tests/functional/openlp_plugins/images/test_lib.py'
--- tests/functional/openlp_plugins/images/test_lib.py	2017-05-08 19:04:14 +0000
+++ tests/functional/openlp_plugins/images/test_lib.py	2017-08-27 17:50:51 +0000
@@ -28,6 +28,7 @@
 from PyQt5 import QtCore, QtWidgets
 
 from openlp.core.common import Registry
+from openlp.core.common.path import Path
 from openlp.plugins.images.lib.db import ImageFilenames, ImageGroups
 from openlp.plugins.images.lib.mediaitem import ImageMediaItem
 
@@ -65,7 +66,7 @@
         # THEN: load_list should have been called with the file list and None,
         #       the directory should have been saved to the settings
         mocked_load_list.assert_called_once_with(file_list, None)
-        mocked_settings().setValue.assert_called_once_with(ANY, '/path1')
+        mocked_settings().setValue.assert_called_once_with(ANY, Path('/', 'path1'))
 
     @patch('openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_list')
     @patch('openlp.plugins.images.lib.mediaitem.Settings')
@@ -82,7 +83,7 @@
         # THEN: load_list should have been called with the file list and the group name,
         #       the directory should have been saved to the settings
         mocked_load_list.assert_called_once_with(file_list, 'group')
-        mocked_settings().setValue.assert_called_once_with(ANY, '/path1')
+        mocked_settings().setValue.assert_called_once_with(ANY, Path('/', 'path1'))
 
     @patch('openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list')
     def test_save_new_images_list_empty_list(self, mocked_load_full_list):

=== modified file 'tests/functional/openlp_plugins/media/test_mediaitem.py'
--- tests/functional/openlp_plugins/media/test_mediaitem.py	2017-04-24 05:17:55 +0000
+++ tests/functional/openlp_plugins/media/test_mediaitem.py	2017-08-27 17:50:51 +0000
@@ -28,6 +28,7 @@
 from PyQt5 import QtCore
 
 from openlp.core import Settings
+from openlp.core.common.path import Path
 from openlp.plugins.media.lib.mediaitem import MediaMediaItem
 
 from tests.helpers.testmixin import TestMixin
@@ -66,7 +67,7 @@
         Media Remote Search Successful find
         """
         # GIVEN: The Mediaitem set up a list of media
-        Settings().setValue(self.media_item.settings_section + '/media files', ['test.mp3', 'test.mp4'])
+        Settings().setValue(self.media_item.settings_section + '/media files', [Path('test.mp3'), Path('test.mp4')])
         # WHEN: Retrieving the test file
         result = self.media_item.search('test.mp4', False)
         # THEN: a file should be found
@@ -77,7 +78,7 @@
         Media Remote Search not find
         """
         # GIVEN: The Mediaitem set up a list of media
-        Settings().setValue(self.media_item.settings_section + '/media files', ['test.mp3', 'test.mp4'])
+        Settings().setValue(self.media_item.settings_section + '/media files', [Path('test.mp3'), Path('test.mp4')])
         # WHEN: Retrieving the test file
         result = self.media_item.search('test.mpx', False)
         # THEN: a file should be found

=== modified file 'tests/functional/openlp_plugins/presentations/test_presentationcontroller.py'
--- tests/functional/openlp_plugins/presentations/test_presentationcontroller.py	2017-08-12 17:45:56 +0000
+++ tests/functional/openlp_plugins/presentations/test_presentationcontroller.py	2017-08-27 17:50:51 +0000
@@ -24,10 +24,10 @@
 classes and related methods.
 """
 import os
-from pathlib import Path
 from unittest import TestCase
 from unittest.mock import MagicMock, mock_open, patch
 
+from openlp.core.common.path import Path
 from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
 
 FOLDER_TO_PATCH = 'openlp.plugins.presentations.lib.presentationcontroller.PresentationDocument.get_thumbnail_folder'

=== modified file 'tests/interfaces/openlp_core_common/test_utils.py'
--- tests/interfaces/openlp_core_common/test_utils.py	2017-08-12 17:45:56 +0000
+++ tests/interfaces/openlp_core_common/test_utils.py	2017-08-27 17:50:51 +0000
@@ -22,10 +22,10 @@
 """
 Functional tests to test the AppLocation class and related methods.
 """
-from pathlib import Path
 from unittest import TestCase
 
 from openlp.core.common import is_not_image_file
+from openlp.core.common.path import Path
 from tests.utils.constants import TEST_RESOURCES_PATH
 from tests.helpers.testmixin import TestMixin
 

=== modified file 'tests/interfaces/openlp_core_lib/test_pluginmanager.py'
--- tests/interfaces/openlp_core_lib/test_pluginmanager.py	2017-04-24 05:17:55 +0000
+++ tests/interfaces/openlp_core_lib/test_pluginmanager.py	2017-08-27 17:50:51 +0000
@@ -32,6 +32,7 @@
 from PyQt5 import QtWidgets
 
 from openlp.core.common import Registry, Settings
+from openlp.core.common.path import Path
 from openlp.core.lib.pluginmanager import PluginManager
 
 from tests.helpers.testmixin import TestMixin
@@ -48,7 +49,7 @@
         """
         self.setup_application()
         self.build_settings()
-        self.temp_dir = mkdtemp('openlp')
+        self.temp_dir = Path(mkdtemp('openlp'))
         Settings().setValue('advanced/data path', self.temp_dir)
         Registry.create()
         Registry().register('service_list', MagicMock())
@@ -62,7 +63,7 @@
         # On windows we need to manually garbage collect to close sqlalchemy files
         # to avoid errors when temporary files are deleted.
         gc.collect()
-        shutil.rmtree(self.temp_dir)
+        shutil.rmtree(str(self.temp_dir))
 
     @patch('openlp.plugins.songusage.lib.db.init_schema')
     @patch('openlp.plugins.songs.lib.db.init_schema')


Follow ups