← Back to team overview

openlp-core team mailing list archive

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

 

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

Requested reviews:
  OpenLP Core (openlp-core)

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

pathlib changes to the theme code

lp:~phill-ridout/openlp/pathlib7 (revision 2773)
[SUCCESS] https://ci.openlp.io/job/Branch-01-Pull/2214/
[SUCCESS] https://ci.openlp.io/job/Branch-02-Functional-Tests/2117/
[SUCCESS] https://ci.openlp.io/job/Branch-03-Interface-Tests/2003/
[SUCCESS] https://ci.openlp.io/job/Branch-04a-Code_Analysis/1369/
[SUCCESS] https://ci.openlp.io/job/Branch-04b-Test_Coverage/1200/
-- 
Your team OpenLP Core is requested to review the proposed merge of lp:~phill-ridout/openlp/pathlib7 into lp:openlp.
=== modified file 'openlp/core/lib/json/theme.json'
--- openlp/core/lib/json/theme.json	2013-10-18 18:10:47 +0000
+++ openlp/core/lib/json/theme.json	2017-09-26 17:09:04 +0000
@@ -4,7 +4,7 @@
         "color": "#000000",
         "direction": "vertical",
         "end_color": "#000000",
-        "filename": "",
+        "filename": null,
         "start_color": "#000000",
         "type": "solid"
     },

=== modified file 'openlp/core/lib/renderer.py'
--- openlp/core/lib/renderer.py	2017-01-25 21:17:27 +0000
+++ openlp/core/lib/renderer.py	2017-09-26 17:09:04 +0000
@@ -26,6 +26,7 @@
 from PyQt5 import QtGui, QtCore, QtWebKitWidgets
 
 from openlp.core.common import Registry, RegistryProperties, OpenLPMixin, RegistryMixin, Settings
+from openlp.core.common.path import path_to_str
 from openlp.core.lib import FormattingTags, ImageSource, ItemCapabilities, ScreenList, ServiceItem, expand_tags, \
     build_lyrics_format_css, build_lyrics_outline_css, build_chords_css
 from openlp.core.common import ThemeLevel
@@ -118,7 +119,7 @@
             theme_data, main_rect, footer_rect = self._theme_dimensions[theme_name]
         # if No file do not update cache
         if theme_data.background_filename:
-            self.image_manager.add_image(theme_data.background_filename,
+            self.image_manager.add_image(path_to_str(theme_data.background_filename),
                                          ImageSource.Theme, QtGui.QColor(theme_data.background_border_color))
 
     def pre_render(self, override_theme_data=None):
@@ -207,8 +208,8 @@
         service_item.raw_footer = FOOTER
         # if No file do not update cache
         if theme_data.background_filename:
-            self.image_manager.add_image(
-                theme_data.background_filename, ImageSource.Theme, QtGui.QColor(theme_data.background_border_color))
+            self.image_manager.add_image(path_to_str(theme_data.background_filename),
+                                         ImageSource.Theme, QtGui.QColor(theme_data.background_border_color))
         theme_data, main, footer = self.pre_render(theme_data)
         service_item.theme_data = theme_data
         service_item.main = main

=== modified file 'openlp/core/lib/theme.py'
--- openlp/core/lib/theme.py	2017-08-12 17:45:56 +0000
+++ openlp/core/lib/theme.py	2017-09-26 17:09:04 +0000
@@ -22,13 +22,13 @@
 """
 Provide the theme XML and handling functions for OpenLP v2 themes.
 """
-import os
+import json
 import logging
-import json
 
 from lxml import etree, objectify
 from openlp.core.common import AppLocation, de_hump
-
+from openlp.core.common.json import OpenLPJsonDecoder, OpenLPJsonEncoder
+from openlp.core.common.path import Path, str_to_path
 from openlp.core.lib import str_to_bool, ScreenList, get_text_file_string
 
 log = logging.getLogger(__name__)
@@ -160,9 +160,8 @@
         # basic theme object with defaults
         json_path = AppLocation.get_directory(AppLocation.AppDir) / 'core' / 'lib' / 'json' / 'theme.json'
         jsn = get_text_file_string(json_path)
-        jsn = json.loads(jsn)
-        self.expand_json(jsn)
-        self.background_filename = ''
+        self.load_theme(jsn)
+        self.background_filename = None
 
     def expand_json(self, var, prev=None):
         """
@@ -174,8 +173,6 @@
         for key, value in var.items():
             if prev:
                 key = prev + "_" + key
-            else:
-                key = key
             if isinstance(value, dict):
                 self.expand_json(value, key)
             else:
@@ -185,13 +182,13 @@
         """
         Add the path name to the image name so the background can be rendered.
 
-        :param path: The path name to be added.
+        :param openlp.core.common.path.Path path: The path name to be added.
+        :rtype: None
         """
         if self.background_type == 'image' or self.background_type == 'video':
             if self.background_filename and path:
                 self.theme_name = self.theme_name.strip()
-                self.background_filename = self.background_filename.strip()
-                self.background_filename = os.path.join(path, self.theme_name, self.background_filename)
+                self.background_filename = path / self.theme_name / self.background_filename
 
     def set_default_header_footer(self):
         """
@@ -206,16 +203,21 @@
         self.font_footer_y = current_screen['size'].height() * 9 / 10
         self.font_footer_height = current_screen['size'].height() / 10
 
-    def load_theme(self, theme):
+    def load_theme(self, theme, theme_path=None):
         """
         Convert the JSON file and expand it.
 
         :param theme: the theme string
+        :param openlp.core.common.path.Path theme_path: The path to the theme
+        :rtype: None
         """
-        jsn = json.loads(theme)
+        if theme_path:
+            jsn = json.loads(theme, cls=OpenLPJsonDecoder, base_path=theme_path)
+        else:
+            jsn = json.loads(theme, cls=OpenLPJsonDecoder)
         self.expand_json(jsn)
 
-    def export_theme(self):
+    def export_theme(self, theme_path=None):
         """
         Loop through the fields and build a dictionary of them
 
@@ -223,7 +225,9 @@
         theme_data = {}
         for attr, value in self.__dict__.items():
             theme_data["{attr}".format(attr=attr)] = value
-        return json.dumps(theme_data)
+        if theme_path:
+            return json.dumps(theme_data, cls=OpenLPJsonEncoder, base_path=theme_path)
+        return json.dumps(theme_data, cls=OpenLPJsonEncoder)
 
     def parse(self, xml):
         """

=== modified file 'openlp/core/ui/maindisplay.py'
--- openlp/core/ui/maindisplay.py	2017-09-05 04:28:50 +0000
+++ openlp/core/ui/maindisplay.py	2017-09-26 17:09:04 +0000
@@ -346,7 +346,7 @@
         if not hasattr(self, 'service_item'):
             return False
         self.override['image'] = path
-        self.override['theme'] = self.service_item.theme_data.background_filename
+        self.override['theme'] = path_to_str(self.service_item.theme_data.background_filename)
         self.image(path)
         # Update the preview frame.
         if self.is_live:
@@ -454,7 +454,7 @@
                 Registry().execute('video_background_replaced')
                 self.override = {}
             # We have a different theme.
-            elif self.override['theme'] != service_item.theme_data.background_filename:
+            elif self.override['theme'] != path_to_str(service_item.theme_data.background_filename):
                 Registry().execute('live_theme_changed')
                 self.override = {}
             else:
@@ -466,7 +466,7 @@
         if self.service_item.theme_data.background_type == 'image':
             if self.service_item.theme_data.background_filename:
                 self.service_item.bg_image_bytes = self.image_manager.get_image_bytes(
-                    self.service_item.theme_data.background_filename, ImageSource.Theme)
+                    path_to_str(self.service_item.theme_data.background_filename), ImageSource.Theme)
             if image_path:
                 image_bytes = self.image_manager.get_image_bytes(image_path, ImageSource.ImagePlugin)
         created_html = build_html(self.service_item, self.screen, self.is_live, background, image_bytes,
@@ -488,7 +488,7 @@
                 path = os.path.join(str(AppLocation.get_section_data_path('themes')),
                                     self.service_item.theme_data.theme_name)
                 service_item.add_from_command(path,
-                                              self.service_item.theme_data.background_filename,
+                                              path_to_str(self.service_item.theme_data.background_filename),
                                               ':/media/slidecontroller_multimedia.png')
                 self.media_controller.video(DisplayControllerType.Live, service_item, video_behind_text=True)
         self._hide_mouse()

=== modified file 'openlp/core/ui/themeform.py'
--- openlp/core/ui/themeform.py	2017-08-25 20:03:25 +0000
+++ openlp/core/ui/themeform.py	2017-09-26 17:09:04 +0000
@@ -28,7 +28,6 @@
 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, 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
@@ -61,7 +60,7 @@
         self.setupUi(self)
         self.registerFields()
         self.update_theme_allowed = True
-        self.temp_background_filename = ''
+        self.temp_background_filename = None
         self.theme_layout_form = ThemeLayoutForm(self)
         self.background_combo_box.currentIndexChanged.connect(self.on_background_combo_box_current_index_changed)
         self.gradient_combo_box.currentIndexChanged.connect(self.on_gradient_combo_box_current_index_changed)
@@ -188,8 +187,7 @@
         """
         background_image = BackgroundType.to_string(BackgroundType.Image)
         if self.page(self.currentId()) == self.background_page and \
-                self.theme.background_type == background_image and \
-                is_not_image_file(Path(self.theme.background_filename)):
+                self.theme.background_type == background_image and is_not_image_file(self.theme.background_filename):
             QtWidgets.QMessageBox.critical(self, translate('OpenLP.ThemeWizard', 'Background Image Empty'),
                                            translate('OpenLP.ThemeWizard', 'You have not selected a '
                                                      'background image. Please select one before continuing.'))
@@ -273,7 +271,7 @@
         Run the wizard.
         """
         log.debug('Editing theme {name}'.format(name=self.theme.theme_name))
-        self.temp_background_filename = ''
+        self.temp_background_filename = None
         self.update_theme_allowed = False
         self.set_defaults()
         self.update_theme_allowed = True
@@ -318,11 +316,11 @@
             self.setField('background_type', 1)
         elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Image):
             self.image_color_button.color = self.theme.background_border_color
-            self.image_path_edit.path = str_to_path(self.theme.background_filename)
+            self.image_path_edit.path = self.theme.background_filename
             self.setField('background_type', 2)
         elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Video):
             self.video_color_button.color = self.theme.background_border_color
-            self.video_path_edit.path = str_to_path(self.theme.background_filename)
+            self.video_path_edit.path = self.theme.background_filename
             self.setField('background_type', 4)
         elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Transparent):
             self.setField('background_type', 3)
@@ -402,14 +400,14 @@
             self.theme.background_type = BackgroundType.to_string(index)
             if self.theme.background_type != BackgroundType.to_string(BackgroundType.Image) and \
                     self.theme.background_type != BackgroundType.to_string(BackgroundType.Video) and \
-                    self.temp_background_filename == '':
+                    self.temp_background_filename is None:
                 self.temp_background_filename = self.theme.background_filename
-                self.theme.background_filename = ''
+                self.theme.background_filename = None
             if (self.theme.background_type == BackgroundType.to_string(BackgroundType.Image) or
                     self.theme.background_type != BackgroundType.to_string(BackgroundType.Video)) and \
-                    self.temp_background_filename != '':
+                    self.temp_background_filename is not None:
                 self.theme.background_filename = self.temp_background_filename
-                self.temp_background_filename = ''
+                self.temp_background_filename = None
             self.set_background_page_values()
 
     def on_gradient_combo_box_current_index_changed(self, index):
@@ -450,18 +448,24 @@
         """
         self.theme.background_end_color = color
 
-    def on_image_path_edit_path_changed(self, file_path):
-        """
-        Background Image button pushed.
-        """
-        self.theme.background_filename = path_to_str(file_path)
+    def on_image_path_edit_path_changed(self, new_path):
+        """
+        Handle the `pathEditChanged` signal from image_path_edit
+
+        :param openlp.core.common.path.Path new_path: Path to the new image
+        :rtype: None
+        """
+        self.theme.background_filename = new_path
         self.set_background_page_values()
 
-    def on_video_path_edit_path_changed(self, file_path):
-        """
-        Background video button pushed.
-        """
-        self.theme.background_filename = path_to_str(file_path)
+    def on_video_path_edit_path_changed(self, new_path):
+        """
+        Handle the `pathEditChanged` signal from video_path_edit
+
+        :param openlp.core.common.path.Path new_path: Path to the new video
+        :rtype: None
+        """
+        self.theme.background_filename = new_path
         self.set_background_page_values()
 
     def on_main_color_changed(self, color):
@@ -537,14 +541,14 @@
                 translate('OpenLP.ThemeWizard', 'Theme Name Invalid'),
                 translate('OpenLP.ThemeWizard', 'Invalid theme name. Please enter one.'))
             return
-        save_from = None
-        save_to = None
+        source_path = None
+        destination_path = None
         if self.theme.background_type == BackgroundType.to_string(BackgroundType.Image) or \
            self.theme.background_type == BackgroundType.to_string(BackgroundType.Video):
-            filename = os.path.split(str(self.theme.background_filename))[1]
-            save_to = os.path.join(self.path, self.theme.theme_name, filename)
-            save_from = self.theme.background_filename
+            file_name = self.theme.background_filename.name
+            destination_path = self.path / self.theme.theme_name / file_name
+            source_path = self.theme.background_filename
         if not self.edit_mode and not self.theme_manager.check_if_theme_exists(self.theme.theme_name):
             return
-        self.theme_manager.save_theme(self.theme, save_from, save_to)
+        self.theme_manager.save_theme(self.theme, source_path, destination_path)
         return QtWidgets.QDialog.accept(self)

=== modified file 'openlp/core/ui/thememanager.py'
--- openlp/core/ui/thememanager.py	2017-09-17 19:43:15 +0000
+++ openlp/core/ui/thememanager.py	2017-09-26 17:09:04 +0000
@@ -24,14 +24,14 @@
 """
 import os
 import zipfile
-import shutil
-
 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, path_to_str, str_to_path
+    UiStrings, check_directory_exists, translate, delete_file
+from openlp.core.common.languagemanager import get_locale_key
+from openlp.core.common.path import Path, copyfile, path_to_str, rmtree
 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
@@ -39,7 +39,6 @@
 from openlp.core.ui import FileRenameForm, ThemeForm
 from openlp.core.ui.lib import OpenLPToolbar
 from openlp.core.ui.lib.filedialog import FileDialog
-from openlp.core.common.languagemanager import get_locale_key
 
 
 class Ui_ThemeManager(object):
@@ -135,7 +134,7 @@
         self.settings_section = 'themes'
         # Variables
         self.theme_list = []
-        self.old_background_image = None
+        self.old_background_image_path = None
 
     def bootstrap_initialise(self):
         """
@@ -145,25 +144,41 @@
         self.global_theme = Settings().value(self.settings_section + '/global theme')
         self.build_theme_path()
         self.load_first_time_themes()
+        self.upgrade_themes()
 
     def bootstrap_post_set_up(self):
         """
         process the bootstrap post setup request
         """
         self.theme_form = ThemeForm(self)
-        self.theme_form.path = self.path
+        self.theme_form.path = self.theme_path
         self.file_rename_form = FileRenameForm()
         Registry().register_function('theme_update_global', self.change_global_from_tab)
         self.load_themes()
 
+    def upgrade_themes(self):
+        """
+        Upgrade the xml files to json.
+
+        :rtype: None
+        """
+        xml_file_paths = AppLocation.get_section_data_path('themes').glob('*/*.xml')
+        for xml_file_path in xml_file_paths:
+            theme_data = get_text_file_string(xml_file_path)
+            theme = self._create_theme_from_xml(theme_data, self.theme_path)
+            self._write_theme(theme)
+            xml_file_path.unlink()
+
     def build_theme_path(self):
         """
         Set up the theme path variables
+
+        :rtype: None
         """
-        self.path = str(AppLocation.get_section_data_path(self.settings_section))
-        check_directory_exists(Path(self.path))
-        self.thumb_path = os.path.join(self.path, 'thumbnails')
-        check_directory_exists(Path(self.thumb_path))
+        self.theme_path = AppLocation.get_section_data_path(self.settings_section)
+        check_directory_exists(self.theme_path)
+        self.thumb_path = self.theme_path / 'thumbnails'
+        check_directory_exists(self.thumb_path)
 
     def check_list_state(self, item, field=None):
         """
@@ -298,17 +313,18 @@
         """
         Takes a theme and makes a new copy of it as well as saving it.
 
-        :param theme_data: The theme to be used
-        :param new_theme_name: The new theme name to save the data to
+        :param Theme theme_data: The theme to be used
+        :param str new_theme_name: The new theme name of the theme
+        :rtype: None
         """
-        save_to = None
-        save_from = None
+        destination_path = None
+        source_path = None
         if theme_data.background_type == 'image' or theme_data.background_type == 'video':
-            save_to = os.path.join(self.path, new_theme_name, os.path.split(str(theme_data.background_filename))[1])
-            save_from = theme_data.background_filename
+            destination_path = self.theme_path / new_theme_name / theme_data.background_filename.name
+            source_path = theme_data.background_filename
         theme_data.theme_name = new_theme_name
-        theme_data.extend_image_filename(self.path)
-        self.save_theme(theme_data, save_from, save_to)
+        theme_data.extend_image_filename(self.theme_path)
+        self.save_theme(theme_data, source_path, destination_path)
         self.load_themes()
 
     def on_edit_theme(self, field=None):
@@ -322,10 +338,10 @@
             item = self.theme_list_widget.currentItem()
             theme = self.get_theme_data(item.data(QtCore.Qt.UserRole))
             if theme.background_type == 'image' or theme.background_type == 'video':
-                self.old_background_image = theme.background_filename
+                self.old_background_image_path = theme.background_filename
             self.theme_form.theme = theme
             self.theme_form.exec(True)
-            self.old_background_image = None
+            self.old_background_image_path = None
             self.renderer.update_theme(theme.theme_name)
             self.load_themes()
 
@@ -355,77 +371,76 @@
         """
         self.theme_list.remove(theme)
         thumb = '{name}.png'.format(name=theme)
-        delete_file(Path(self.path, thumb))
-        delete_file(Path(self.thumb_path, thumb))
+        delete_file(self.theme_path / thumb)
+        delete_file(self.thumb_path / thumb)
         try:
-            # Windows is always unicode, so no need to encode filenames
-            if is_win():
-                shutil.rmtree(os.path.join(self.path, theme))
-            else:
-                encoding = get_filesystem_encoding()
-                shutil.rmtree(os.path.join(self.path, theme).encode(encoding))
-        except OSError as os_error:
-            shutil.Error = os_error
+            rmtree(self.theme_path / theme)
+        except OSError:
             self.log_exception('Error deleting theme {name}'.format(name=theme))
 
-    def on_export_theme(self, field=None):
+    def on_export_theme(self, checked=None):
         """
-        Export the theme in a zip file
-        :param field:
+        Export the theme to a zip file
+
+        :param bool checked: Sent by the QAction.triggered signal. It's not used in this method.
+        :rtype: None
         """
         item = self.theme_list_widget.currentItem()
         if item is None:
             critical_error_message_box(message=translate('OpenLP.ThemeManager', 'You have not selected a theme.'))
             return
-        theme = item.data(QtCore.Qt.UserRole)
+        theme_name = item.data(QtCore.Qt.UserRole)
         export_path, filter_used = \
             FileDialog.getSaveFileName(self.main_window,
-                                       translate('OpenLP.ThemeManager', 'Save Theme - ({name})').
-                                       format(name=theme),
+                                       translate('OpenLP.ThemeManager',
+                                                 'Save Theme - ({name})').format(name=theme_name),
                                        Settings().value(self.settings_section + '/last directory export'),
+                                       translate('OpenLP.ThemeManager', 'OpenLP Themes (*.otz)'),
                                        translate('OpenLP.ThemeManager', 'OpenLP Themes (*.otz)'))
         self.application.set_busy_cursor()
         if export_path:
             Settings().setValue(self.settings_section + '/last directory export', export_path.parent)
-            if self._export_theme(str(export_path), theme):
+            if self._export_theme(export_path.with_suffix('.otz'), theme_name):
                 QtWidgets.QMessageBox.information(self,
                                                   translate('OpenLP.ThemeManager', 'Theme Exported'),
                                                   translate('OpenLP.ThemeManager',
                                                             'Your theme has been successfully exported.'))
         self.application.set_normal_cursor()
 
-    def _export_theme(self, theme_path, theme):
+    def _export_theme(self, theme_path, theme_name):
         """
         Create the zipfile with the theme contents.
-        :param theme_path: Location where the zip file will be placed
-        :param theme: The name of the theme to be exported
+
+        :param openlp.core.common.path.Path theme_path: Location where the zip file will be placed
+        :param str theme_name: The name of the theme to be exported
+        :return: The success of creating the zip file
+        :rtype: bool
         """
-        theme_zip = None
         try:
-            theme_zip = zipfile.ZipFile(theme_path, 'w')
-            source = os.path.join(self.path, theme)
-            for files in os.walk(source):
-                for name in files[2]:
-                    theme_zip.write(os.path.join(source, name), os.path.join(theme, name))
-            theme_zip.close()
+            with zipfile.ZipFile(str(theme_path), 'w') as theme_zip:
+                source_path = self.theme_path / theme_name
+                for file_path in source_path.iterdir():
+                    theme_zip.write(str(file_path), os.path.join(theme_name, file_path.name))
             return True
         except OSError as ose:
             self.log_exception('Export Theme Failed')
             critical_error_message_box(translate('OpenLP.ThemeManager', 'Theme Export Failed'),
-                                       translate('OpenLP.ThemeManager', 'The theme export failed because this error '
-                                                                        'occurred: {err}').format(err=ose.strerror))
-            if theme_zip:
-                theme_zip.close()
-                shutil.rmtree(theme_path, True)
+                                       translate('OpenLP.ThemeManager',
+                                                 'The theme_name export failed because this error occurred: {err}')
+                                       .format(err=ose.strerror))
+            if theme_path.exists():
+                rmtree(theme_path, True)
             return False
 
-    def on_import_theme(self, field=None):
+    def on_import_theme(self, checked=None):
         """
         Opens a file dialog to select the theme file(s) to import before attempting to extract OpenLP themes from
         those files. This process will only load version 2 themes.
-        :param field:
+
+        :param bool checked: Sent by the QAction.triggered signal. It's not used in this method.
+        :rtype: None
         """
-        file_paths, selected_filter = FileDialog.getOpenFileNames(
+        file_paths, filter_used = FileDialog.getOpenFileNames(
             self,
             translate('OpenLP.ThemeManager', 'Select Theme Import File'),
             Settings().value(self.settings_section + '/last directory import'),
@@ -435,8 +450,8 @@
             return
         self.application.set_busy_cursor()
         for file_path in file_paths:
-            self.unzip_theme(path_to_str(file_path), self.path)
-        Settings().setValue(self.settings_section + '/last directory import', file_path)
+            self.unzip_theme(file_path, self.theme_path)
+        Settings().setValue(self.settings_section + '/last directory import', file_path.parent)
         self.load_themes()
         self.application.set_normal_cursor()
 
@@ -445,17 +460,17 @@
         Imports any themes on start up and makes sure there is at least one theme
         """
         self.application.set_busy_cursor()
-        files = AppLocation.get_files(self.settings_section, '.otz')
-        for theme_file in files:
-            theme_file = os.path.join(self.path, str(theme_file))
-            self.unzip_theme(theme_file, self.path)
-            delete_file(Path(theme_file))
-        files = AppLocation.get_files(self.settings_section, '.png')
+        theme_paths = AppLocation.get_files(self.settings_section, '.otz')
+        for theme_path in theme_paths:
+            theme_path = self.theme_path / theme_path
+            self.unzip_theme(theme_path, self.theme_path)
+            delete_file(theme_path)
+        theme_paths = AppLocation.get_files(self.settings_section, '.png')
         # No themes have been found so create one
-        if not files:
+        if not theme_paths:
             theme = Theme()
             theme.theme_name = UiStrings().Default
-            self._write_theme(theme, None, None)
+            self._write_theme(theme)
             Settings().setValue(self.settings_section + '/global theme', theme.theme_name)
         self.application.set_normal_cursor()
 
@@ -471,22 +486,21 @@
         # Sort the themes by its name considering language specific
         files.sort(key=lambda file_name: get_locale_key(str(file_name)))
         # now process the file list of png files
-        for name in files:
-            name = str(name)
+        for file in files:
             # check to see file is in theme root directory
-            theme = os.path.join(self.path, name)
-            if os.path.exists(theme):
-                text_name = os.path.splitext(name)[0]
+            theme_path = self.theme_path / file
+            if theme_path.exists():
+                text_name = theme_path.stem
                 if text_name == self.global_theme:
                     name = translate('OpenLP.ThemeManager', '{name} (default)').format(name=text_name)
                 else:
                     name = text_name
-                thumb = os.path.join(self.thumb_path, '{name}.png'.format(name=text_name))
+                thumb = self.thumb_path / '{name}.png'.format(name=text_name)
                 item_name = QtWidgets.QListWidgetItem(name)
-                if validate_thumb(Path(theme), Path(thumb)):
+                if validate_thumb(theme_path, thumb):
                     icon = build_icon(thumb)
                 else:
-                    icon = create_thumb(theme, thumb)
+                    icon = create_thumb(str(theme_path), str(thumb))
                 item_name.setIcon(icon)
                 item_name.setData(QtCore.Qt.UserRole, text_name)
                 self.theme_list_widget.addItem(item_name)
@@ -507,27 +521,19 @@
 
     def get_theme_data(self, theme_name):
         """
-        Returns a theme object from an XML or JSON file
+        Returns a theme object from a JSON file
 
-        :param theme_name: Name of the theme to load from file
-        :return: The theme object.
+        :param str theme_name: Name of the theme to load from file
+        :return:  The theme object.
+        :rtype: Theme
         """
-        self.log_debug('get theme data for theme {name}'.format(name=theme_name))
-        theme_file_path = Path(self.path, str(theme_name), '{file_name}.json'.format(file_name=theme_name))
+        theme_name = str(theme_name)
+        theme_file_path = self.theme_path / theme_name / '{file_name}.json'.format(file_name=theme_name)
         theme_data = get_text_file_string(theme_file_path)
-        jsn = True
-        if not theme_data:
-            theme_file_path = theme_file_path.with_suffix('.xml')
-            theme_data = get_text_file_string(theme_file_path)
-            jsn = False
         if not theme_data:
             self.log_debug('No theme data - using default theme')
             return Theme()
-        else:
-            if jsn:
-                return self._create_theme_from_json(theme_data, self.path)
-            else:
-                return self._create_theme_from_xml(theme_data, self.path)
+        return self._create_theme_from_json(theme_data, self.theme_path)
 
     def over_write_message_box(self, theme_name):
         """
@@ -543,172 +549,148 @@
                                              defaultButton=QtWidgets.QMessageBox.No)
         return ret == QtWidgets.QMessageBox.Yes
 
-    def unzip_theme(self, file_name, directory):
+    def unzip_theme(self, file_path, directory_path):
         """
         Unzip the theme, remove the preview file if stored. Generate a new preview file. Check the XML theme version
         and upgrade if necessary.
-        :param file_name:
-        :param directory:
+        :param openlp.core.common.path.Path file_path:
+        :param openlp.core.common.path.Path directory_path:
         """
-        self.log_debug('Unzipping theme {name}'.format(name=file_name))
-        theme_zip = None
-        out_file = None
+        self.log_debug('Unzipping theme {name}'.format(name=file_path))
         file_xml = None
         abort_import = True
         json_theme = False
         theme_name = ""
         try:
-            theme_zip = zipfile.ZipFile(file_name)
-            json_file = [name for name in theme_zip.namelist() if os.path.splitext(name)[1].lower() == '.json']
-            if len(json_file) != 1:
-                # TODO: remove XML handling at some point but would need a auto conversion to run first.
-                xml_file = [name for name in theme_zip.namelist() if os.path.splitext(name)[1].lower() == '.xml']
-                if len(xml_file) != 1:
-                    self.log_error('Theme contains "{val:d}" theme files'.format(val=len(xml_file)))
-                    raise ValidationError
-                xml_tree = ElementTree(element=XML(theme_zip.read(xml_file[0]))).getroot()
-                theme_version = xml_tree.get('version', default=None)
-                if not theme_version or float(theme_version) < 2.0:
-                    self.log_error('Theme version is less than 2.0')
-                    raise ValidationError
-                theme_name = xml_tree.find('name').text.strip()
-            else:
-                new_theme = Theme()
-                new_theme.load_theme(theme_zip.read(json_file[0]).decode("utf-8"))
-                theme_name = new_theme.theme_name
-                json_theme = True
-            theme_folder = os.path.join(directory, theme_name)
-            theme_exists = os.path.exists(theme_folder)
-            if theme_exists and not self.over_write_message_box(theme_name):
-                abort_import = True
-                return
-            else:
-                abort_import = False
-            for name in theme_zip.namelist():
-                out_name = name.replace('/', os.path.sep)
-                split_name = out_name.split(os.path.sep)
-                if split_name[-1] == '' or len(split_name) == 1:
-                    # is directory or preview file
-                    continue
-                full_name = os.path.join(directory, out_name)
-                check_directory_exists(Path(os.path.dirname(full_name)))
-                if os.path.splitext(name)[1].lower() == '.xml' or os.path.splitext(name)[1].lower() == '.json':
-                    file_xml = str(theme_zip.read(name), 'utf-8')
-                    out_file = open(full_name, 'w', encoding='utf-8')
-                    out_file.write(file_xml)
-                else:
-                    out_file = open(full_name, 'wb')
-                    out_file.write(theme_zip.read(name))
-                out_file.close()
+            with zipfile.ZipFile(str(file_path)) as theme_zip:
+                json_file = [name for name in theme_zip.namelist() if os.path.splitext(name)[1].lower() == '.json']
+                if len(json_file) != 1:
+                    # TODO: remove XML handling after the 2.6 release.
+                    xml_file = [name for name in theme_zip.namelist() if os.path.splitext(name)[1].lower() == '.xml']
+                    if len(xml_file) != 1:
+                        self.log_error('Theme contains "{val:d}" theme files'.format(val=len(xml_file)))
+                        raise ValidationError
+                    xml_tree = ElementTree(element=XML(theme_zip.read(xml_file[0]))).getroot()
+                    theme_version = xml_tree.get('version', default=None)
+                    if not theme_version or float(theme_version) < 2.0:
+                        self.log_error('Theme version is less than 2.0')
+                        raise ValidationError
+                    theme_name = xml_tree.find('name').text.strip()
+                else:
+                    new_theme = Theme()
+                    new_theme.load_theme(theme_zip.read(json_file[0]).decode("utf-8"))
+                    theme_name = new_theme.theme_name
+                    json_theme = True
+                theme_folder = directory_path / theme_name
+                if theme_folder.exists() and not self.over_write_message_box(theme_name):
+                    abort_import = True
+                    return
+                else:
+                    abort_import = False
+                for zipped_file in theme_zip.namelist():
+                    zipped_file_rel_path = Path(zipped_file)
+                    split_name = zipped_file_rel_path.parts
+                    if split_name[-1] == '' or len(split_name) == 1:
+                        # is directory or preview file
+                        continue
+                    full_name = directory_path / zipped_file_rel_path
+                    check_directory_exists(full_name.parent)
+                    if zipped_file_rel_path.suffix.lower() == '.xml' or zipped_file_rel_path.suffix.lower() == '.json':
+                        file_xml = str(theme_zip.read(zipped_file), 'utf-8')
+                        with full_name.open('w', encoding='utf-8') as out_file:
+                            out_file.write(file_xml)
+                    else:
+                        with full_name.open('wb') as out_file:
+                            out_file.write(theme_zip.read(zipped_file))
         except (IOError, zipfile.BadZipfile):
-            self.log_exception('Importing theme from zip failed {name}'.format(name=file_name))
+            self.log_exception('Importing theme from zip failed {name}'.format(name=file_path))
             raise ValidationError
         except ValidationError:
             critical_error_message_box(translate('OpenLP.ThemeManager', 'Validation Error'),
                                        translate('OpenLP.ThemeManager', 'File is not a valid theme.'))
         finally:
-            # Close the files, to be able to continue creating the theme.
-            if theme_zip:
-                theme_zip.close()
-            if out_file:
-                out_file.close()
             if not abort_import:
                 # As all files are closed, we can create the Theme.
                 if file_xml:
                     if json_theme:
-                        theme = self._create_theme_from_json(file_xml, self.path)
+                        theme = self._create_theme_from_json(file_xml, self.theme_path)
                     else:
-                        theme = self._create_theme_from_xml(file_xml, self.path)
+                        theme = self._create_theme_from_xml(file_xml, self.theme_path)
                     self.generate_and_save_image(theme_name, theme)
-                # Only show the error message, when IOError was not raised (in
-                # this case the error message has already been shown).
-                elif theme_zip is not None:
-                    critical_error_message_box(
-                        translate('OpenLP.ThemeManager', 'Validation Error'),
-                        translate('OpenLP.ThemeManager', 'File is not a valid theme.'))
-                    self.log_error('Theme file does not contain XML data {name}'.format(name=file_name))
 
     def check_if_theme_exists(self, theme_name):
         """
         Check if theme already exists and displays error message
 
-        :param theme_name:  Name of the Theme to test
+        :param str theme_name:  Name of the Theme to test
         :return: True or False if theme exists
+        :rtype: bool
         """
-        theme_dir = os.path.join(self.path, theme_name)
-        if os.path.exists(theme_dir):
+        if (self.theme_path / theme_name).exists():
             critical_error_message_box(
                 translate('OpenLP.ThemeManager', 'Validation Error'),
                 translate('OpenLP.ThemeManager', 'A theme with this name already exists.'))
             return False
         return True
 
-    def save_theme(self, theme, image_from, image_to):
+    def save_theme(self, theme, image_source_path, image_destination_path):
         """
         Called by theme maintenance Dialog to save the theme and to trigger the reload of the theme list
 
-        :param theme: The theme data object.
-        :param image_from: Where the theme image is currently located.
-        :param image_to: Where the Theme Image is to be saved to
+        :param Theme theme: The theme data object.
+        :param openlp.core.common.path.Path image_source_path: Where the theme image is currently located.
+        :param openlp.core.common.path.Path image_destination_path: Where the Theme Image is to be saved to
+        :rtype: None
         """
-        self._write_theme(theme, image_from, image_to)
+        self._write_theme(theme, image_source_path, image_destination_path)
         if theme.background_type == BackgroundType.to_string(BackgroundType.Image):
-            self.image_manager.update_image_border(theme.background_filename,
+            self.image_manager.update_image_border(path_to_str(theme.background_filename),
                                                    ImageSource.Theme,
                                                    QtGui.QColor(theme.background_border_color))
             self.image_manager.process_updates()
 
-    def _write_theme(self, theme, image_from, image_to):
+    def _write_theme(self, theme, image_source_path=None, image_destination_path=None):
         """
         Writes the theme to the disk and handles the background image if necessary
 
-        :param theme: The theme data object.
-        :param image_from: Where the theme image is currently located.
-        :param image_to: Where the Theme Image is to be saved to
+        :param Theme theme: The theme data object.
+        :param openlp.core.common.path.Path image_source_path: Where the theme image is currently located.
+        :param openlp.core.common.path.Path image_destination_path: Where the Theme Image is to be saved to
+        :rtype: None
         """
         name = theme.theme_name
-        theme_pretty = theme.export_theme()
-        theme_dir = os.path.join(self.path, name)
-        check_directory_exists(Path(theme_dir))
-        theme_file = os.path.join(theme_dir, name + '.json')
-        if self.old_background_image and image_to != self.old_background_image:
-            delete_file(Path(self.old_background_image))
-        out_file = None
+        theme_pretty = theme.export_theme(self.theme_path)
+        theme_dir = self.theme_path / name
+        check_directory_exists(theme_dir)
+        theme_path = theme_dir / '{file_name}.json'.format(file_name=name)
         try:
-            out_file = open(theme_file, 'w', encoding='utf-8')
-            out_file.write(theme_pretty)
+                theme_path.write_text(theme_pretty)
         except IOError:
             self.log_exception('Saving theme to file failed')
-        finally:
-            if out_file:
-                out_file.close()
-        if image_from and os.path.abspath(image_from) != os.path.abspath(image_to):
-            try:
-                # Windows is always unicode, so no need to encode filenames
-                if is_win():
-                    shutil.copyfile(image_from, image_to)
-                else:
-                    encoding = get_filesystem_encoding()
-                    shutil.copyfile(image_from.encode(encoding), image_to.encode(encoding))
-            except IOError as xxx_todo_changeme:
-                shutil.Error = xxx_todo_changeme
-                self.log_exception('Failed to save theme image')
+        if image_source_path and image_destination_path:
+            if self.old_background_image_path and image_destination_path != self.old_background_image_path:
+                delete_file(self.old_background_image_path)
+            if image_source_path != image_destination_path:
+                try:
+                    copyfile(image_source_path, image_destination_path)
+                except IOError:
+                    self.log_exception('Failed to save theme image')
         self.generate_and_save_image(name, theme)
 
-    def generate_and_save_image(self, name, theme):
+    def generate_and_save_image(self, theme_name, theme):
         """
         Generate and save a preview image
 
-        :param name: The name of the theme.
+        :param str theme_name: The name of the theme.
         :param theme: The theme data object.
         """
         frame = self.generate_image(theme)
-        sample_path_name = os.path.join(self.path, name + '.png')
-        if os.path.exists(sample_path_name):
-            os.unlink(sample_path_name)
-        frame.save(sample_path_name, 'png')
-        thumb = os.path.join(self.thumb_path, '{name}.png'.format(name=name))
-        create_thumb(sample_path_name, thumb, False)
+        sample_path_name = self.theme_path / '{file_name}.png'.format(file_name=theme_name)
+        if sample_path_name.exists():
+            sample_path_name.unlink()
+        frame.save(str(sample_path_name), 'png')
+        thumb_path = self.thumb_path / '{name}.png'.format(name=theme_name)
+        create_thumb(str(sample_path_name), str(thumb_path), False)
 
     def update_preview_images(self):
         """
@@ -730,39 +712,32 @@
         """
         return self.renderer.generate_preview(theme_data, force_page)
 
-    def get_preview_image(self, theme):
-        """
-        Return an image representing the look of the theme
-
-        :param theme: The theme to return the image for.
-        """
-        return os.path.join(self.path, theme + '.png')
-
     @staticmethod
     def _create_theme_from_xml(theme_xml, image_path):
         """
         Return a theme object using information parsed from XML
 
         :param theme_xml: The Theme data object.
-        :param image_path: Where the theme image is stored
+        :param openlp.core.common.path.Path image_path: Where the theme image is stored
         :return: Theme data.
+        :rtype: Theme
         """
         theme = Theme()
         theme.parse(theme_xml)
         theme.extend_image_filename(image_path)
         return theme
 
-    @staticmethod
-    def _create_theme_from_json(theme_json, image_path):
+    def _create_theme_from_json(self, theme_json, image_path):
         """
         Return a theme object using information parsed from JSON
 
         :param theme_json: The Theme data object.
-        :param image_path: Where the theme image is stored
+        :param openlp.core.common.path.Path image_path: Where the theme image is stored
         :return: Theme data.
+        :rtype: Theme
         """
         theme = Theme()
-        theme.load_theme(theme_json)
+        theme.load_theme(theme_json, self.theme_path)
         theme.extend_image_filename(image_path)
         return theme
 

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

=== modified file 'openlp/plugins/images/lib/upgrade.py'
--- openlp/plugins/images/lib/upgrade.py	2017-09-23 13:06:42 +0000
+++ openlp/plugins/images/lib/upgrade.py	2017-09-26 17:09:04 +0000
@@ -48,7 +48,6 @@
     """
     Version 2 upgrade - Move file path from old db to JSON encoded path to new db. Added during 2.5 dev
     """
-    # TODO: Update tests
     log.debug('Starting upgrade_2 for file_path to JSON')
     old_table = Table('image_filenames', metadata, autoload=True)
     if 'file_path' not in [col.name for col in old_table.c.values()]:

=== modified file 'tests/functional/openlp_core_lib/test_theme.py'
--- tests/functional/openlp_core_lib/test_theme.py	2017-05-24 20:04:48 +0000
+++ tests/functional/openlp_core_lib/test_theme.py	2017-09-26 17:09:04 +0000
@@ -22,8 +22,9 @@
 """
 Package to test the openlp.core.lib.theme package.
 """
+import os
+from pathlib import Path
 from unittest import TestCase
-import os
 
 from openlp.core.lib.theme import Theme
 
@@ -79,16 +80,16 @@
         """
         # GIVEN: A theme object
         theme = Theme()
-        theme.theme_name = 'MyBeautifulTheme   '
-        theme.background_filename = '    video.mp4'
+        theme.theme_name = 'MyBeautifulTheme'
+        theme.background_filename = Path('video.mp4')
         theme.background_type = 'video'
-        path = os.path.expanduser('~')
+        path = Path.home()
 
         # WHEN: Theme.extend_image_filename is run
         theme.extend_image_filename(path)
 
         # THEN: The filename of the background should be correct
-        expected_filename = os.path.join(path, 'MyBeautifulTheme', 'video.mp4')
+        expected_filename = path / 'MyBeautifulTheme' / 'video.mp4'
         self.assertEqual(expected_filename, theme.background_filename)
         self.assertEqual('MyBeautifulTheme', theme.theme_name)
 

=== modified file 'tests/functional/openlp_core_ui/test_maindisplay.py'
--- tests/functional/openlp_core_ui/test_maindisplay.py	2017-07-08 13:12:31 +0000
+++ tests/functional/openlp_core_ui/test_maindisplay.py	2017-09-26 17:09:04 +0000
@@ -27,10 +27,10 @@
 
 from PyQt5 import QtCore
 
-from openlp.core.common import Registry, is_macosx, Settings
+from openlp.core.common import Registry, is_macosx
+from openlp.core.common.path import Path
 from openlp.core.lib import ScreenList, PluginManager
 from openlp.core.ui import MainDisplay, AudioPlayer
-from openlp.core.ui.media import MediaController
 from openlp.core.ui.maindisplay import TRANSPARENT_STYLESHEET, OPAQUE_STYLESHEET
 
 from tests.helpers.testmixin import TestMixin
@@ -184,7 +184,7 @@
         self.assertEqual(pyobjc_nsview.window().collectionBehavior(), NSWindowCollectionBehaviorManaged,
                          'Window collection behavior should be NSWindowCollectionBehaviorManaged')
 
-    @patch(u'openlp.core.ui.maindisplay.Settings')
+    @patch('openlp.core.ui.maindisplay.Settings')
     def test_show_display_startup_logo(self, MockedSettings):
         # GIVEN: Mocked show_display, setting for logo visibility
         display = MagicMock()
@@ -204,7 +204,7 @@
         # THEN: setVisible should had been called with "True"
         main_display.setVisible.assert_called_once_with(True)
 
-    @patch(u'openlp.core.ui.maindisplay.Settings')
+    @patch('openlp.core.ui.maindisplay.Settings')
     def test_show_display_hide_startup_logo(self, MockedSettings):
         # GIVEN: Mocked show_display, setting for logo visibility
         display = MagicMock()
@@ -224,8 +224,8 @@
         # THEN: setVisible should had not been called
         main_display.setVisible.assert_not_called()
 
-    @patch(u'openlp.core.ui.maindisplay.Settings')
-    @patch(u'openlp.core.ui.maindisplay.build_html')
+    @patch('openlp.core.ui.maindisplay.Settings')
+    @patch('openlp.core.ui.maindisplay.build_html')
     def test_build_html_no_video(self, MockedSettings, Mocked_build_html):
         # GIVEN: Mocked display
         display = MagicMock()
@@ -252,8 +252,8 @@
         self.assertEquals(main_display.media_controller.video.call_count, 0,
                           'Media Controller video should not have been called')
 
-    @patch(u'openlp.core.ui.maindisplay.Settings')
-    @patch(u'openlp.core.ui.maindisplay.build_html')
+    @patch('openlp.core.ui.maindisplay.Settings')
+    @patch('openlp.core.ui.maindisplay.build_html')
     def test_build_html_video(self, MockedSettings, Mocked_build_html):
         # GIVEN: Mocked display
         display = MagicMock()
@@ -270,7 +270,7 @@
         service_item.theme_data = MagicMock()
         service_item.theme_data.background_type = 'video'
         service_item.theme_data.theme_name = 'name'
-        service_item.theme_data.background_filename = 'background_filename'
+        service_item.theme_data.background_filename = Path('background_filename')
         mocked_plugin = MagicMock()
         display.plugin_manager = PluginManager()
         display.plugin_manager.plugins = [mocked_plugin]

=== modified file 'tests/functional/openlp_core_ui/test_themeform.py'
--- tests/functional/openlp_core_ui/test_themeform.py	2017-08-25 20:03:25 +0000
+++ tests/functional/openlp_core_ui/test_themeform.py	2017-09-26 17:09:04 +0000
@@ -49,5 +49,5 @@
             self.instance.on_image_path_edit_path_changed(Path('/', 'new', 'pat.h'))
 
             # THEN: The theme background file should be set and `set_background_page_values` should have been called
-            self.assertEqual(self.instance.theme.background_filename, '/new/pat.h')
+            self.assertEqual(self.instance.theme.background_filename, Path('/', 'new', 'pat.h'))
             mocked_set_background_page_values.assert_called_once_with()

=== modified file 'tests/functional/openlp_core_ui/test_thememanager.py'
--- tests/functional/openlp_core_ui/test_thememanager.py	2017-08-12 17:45:56 +0000
+++ tests/functional/openlp_core_ui/test_thememanager.py	2017-09-26 17:09:04 +0000
@@ -30,8 +30,9 @@
 
 from PyQt5 import QtWidgets
 
+from openlp.core.common import Registry
+from openlp.core.common.path import Path
 from openlp.core.ui import ThemeManager
-from openlp.core.common import Registry
 
 from tests.utils.constants import TEST_RESOURCES_PATH
 
@@ -57,13 +58,13 @@
         """
         # GIVEN: A new ThemeManager instance.
         theme_manager = ThemeManager()
-        theme_manager.path = os.path.join(TEST_RESOURCES_PATH, 'themes')
+        theme_manager.theme_path = Path(TEST_RESOURCES_PATH, 'themes')
         with patch('zipfile.ZipFile.__init__') as mocked_zipfile_init, \
                 patch('zipfile.ZipFile.write') as mocked_zipfile_write:
             mocked_zipfile_init.return_value = None
 
             # WHEN: The theme is exported
-            theme_manager._export_theme(os.path.join('some', 'path', 'Default.otz'), 'Default')
+            theme_manager._export_theme(Path('some', 'path', 'Default.otz'), 'Default')
 
             # THEN: The zipfile should be created at the given path
             mocked_zipfile_init.assert_called_with(os.path.join('some', 'path', 'Default.otz'), 'w')
@@ -86,57 +87,49 @@
         """
         Test that we don't try to overwrite a theme background image with itself
         """
-        # GIVEN: A new theme manager instance, with mocked builtins.open, shutil.copyfile,
+        # GIVEN: A new theme manager instance, with mocked builtins.open, copyfile,
         #        theme, check_directory_exists and thememanager-attributes.
-        with patch('builtins.open') as mocked_open, \
-                patch('openlp.core.ui.thememanager.shutil.copyfile') as mocked_copyfile, \
+        with patch('openlp.core.ui.thememanager.copyfile') as mocked_copyfile, \
                 patch('openlp.core.ui.thememanager.check_directory_exists'):
-            mocked_open.return_value = MagicMock()
             theme_manager = ThemeManager(None)
             theme_manager.old_background_image = None
             theme_manager.generate_and_save_image = MagicMock()
-            theme_manager.path = ''
+            theme_manager.theme_path = MagicMock()
             mocked_theme = MagicMock()
             mocked_theme.theme_name = 'themename'
             mocked_theme.extract_formatted_xml = MagicMock()
             mocked_theme.extract_formatted_xml.return_value = 'fake_theme_xml'.encode()
 
             # WHEN: Calling _write_theme with path to the same image, but the path written slightly different
-            file_name1 = os.path.join(TEST_RESOURCES_PATH, 'church.jpg')
-            # Do replacement from end of string to avoid problems with path start
-            file_name2 = file_name1[::-1].replace(os.sep, os.sep + os.sep, 2)[::-1]
-            theme_manager._write_theme(mocked_theme, file_name1, file_name2)
+            file_name1 = Path(TEST_RESOURCES_PATH, 'church.jpg')
+            theme_manager._write_theme(mocked_theme, file_name1, file_name1)
 
             # THEN: The mocked_copyfile should not have been called
-            self.assertFalse(mocked_copyfile.called, 'shutil.copyfile should not be called')
+            self.assertFalse(mocked_copyfile.called, 'copyfile should not be called')
 
     def test_write_theme_diff_images(self):
         """
         Test that we do overwrite a theme background image when a new is submitted
         """
-        # GIVEN: A new theme manager instance, with mocked builtins.open, shutil.copyfile,
+        # GIVEN: A new theme manager instance, with mocked builtins.open, copyfile,
         #        theme, check_directory_exists and thememanager-attributes.
-        with patch('builtins.open') as mocked_open, \
-                patch('openlp.core.ui.thememanager.shutil.copyfile') as mocked_copyfile, \
+        with patch('openlp.core.ui.thememanager.copyfile') as mocked_copyfile, \
                 patch('openlp.core.ui.thememanager.check_directory_exists'):
-            mocked_open.return_value = MagicMock()
             theme_manager = ThemeManager(None)
             theme_manager.old_background_image = None
             theme_manager.generate_and_save_image = MagicMock()
-            theme_manager.path = ''
+            theme_manager.theme_path = MagicMock()
             mocked_theme = MagicMock()
             mocked_theme.theme_name = 'themename'
             mocked_theme.filename = "filename"
-            # mocked_theme.extract_formatted_xml = MagicMock()
-            # mocked_theme.extract_formatted_xml.return_value = 'fake_theme_xml'.encode()
 
             # WHEN: Calling _write_theme with path to different images
-            file_name1 = os.path.join(TEST_RESOURCES_PATH, 'church.jpg')
-            file_name2 = os.path.join(TEST_RESOURCES_PATH, 'church2.jpg')
+            file_name1 = Path(TEST_RESOURCES_PATH, 'church.jpg')
+            file_name2 = Path(TEST_RESOURCES_PATH, 'church2.jpg')
             theme_manager._write_theme(mocked_theme, file_name1, file_name2)
 
             # THEN: The mocked_copyfile should not have been called
-            self.assertTrue(mocked_copyfile.called, 'shutil.copyfile should be called')
+            self.assertTrue(mocked_copyfile.called, 'copyfile should be called')
 
     def test_write_theme_special_char_name(self):
         """
@@ -146,7 +139,7 @@
         theme_manager = ThemeManager(None)
         theme_manager.old_background_image = None
         theme_manager.generate_and_save_image = MagicMock()
-        theme_manager.path = self.temp_folder
+        theme_manager.theme_path = Path(self.temp_folder)
         mocked_theme = MagicMock()
         mocked_theme.theme_name = 'theme 愛 name'
         mocked_theme.export_theme.return_value = "{}"
@@ -208,17 +201,17 @@
             theme_manager = ThemeManager(None)
             theme_manager._create_theme_from_xml = MagicMock()
             theme_manager.generate_and_save_image = MagicMock()
-            theme_manager.path = ''
-            folder = mkdtemp()
-            theme_file = os.path.join(TEST_RESOURCES_PATH, 'themes', 'Moss_on_tree.otz')
+            theme_manager.theme_path = None
+            folder = Path(mkdtemp())
+            theme_file = Path(TEST_RESOURCES_PATH, 'themes', 'Moss_on_tree.otz')
 
             # WHEN: We try to unzip it
             theme_manager.unzip_theme(theme_file, folder)
 
             # THEN: Files should be unpacked
-            self.assertTrue(os.path.exists(os.path.join(folder, 'Moss on tree', 'Moss on tree.xml')))
+            self.assertTrue((folder / 'Moss on tree' / 'Moss on tree.xml').exists())
             self.assertEqual(mocked_critical_error_message_box.call_count, 0, 'No errors should have happened')
-            shutil.rmtree(folder)
+            shutil.rmtree(str(folder))
 
     def test_unzip_theme_invalid_version(self):
         """

=== modified file 'tests/interfaces/openlp_core_ui/test_thememanager.py'
--- tests/interfaces/openlp_core_ui/test_thememanager.py	2017-04-24 05:17:55 +0000
+++ tests/interfaces/openlp_core_ui/test_thememanager.py	2017-09-26 17:09:04 +0000
@@ -26,7 +26,8 @@
 from unittest.mock import patch, MagicMock
 
 from openlp.core.common import Registry, Settings
-from openlp.core.ui import ThemeManager, ThemeForm, FileRenameForm
+from openlp.core.common.path import Path
+from openlp.core.ui import ThemeManager
 
 from tests.helpers.testmixin import TestMixin
 
@@ -91,6 +92,23 @@
         assert self.theme_manager.thumb_path.startswith(self.theme_manager.path) is True, \
             'The thumb path and the main path should start with the same value'
 
+    def test_build_theme_path(self):
+        """
+        Test the thememanager build_theme_path - basic test
+        """
+        # GIVEN: A new a call to initialise
+        with patch('openlp.core.common.AppLocation.get_section_data_path', return_value=Path('test/path')):
+            Settings().setValue('themes/global theme', 'my_theme')
+
+            self.theme_manager.theme_form = MagicMock()
+            self.theme_manager.load_first_time_themes = MagicMock()
+
+            # WHEN: the build_theme_path is run
+            self.theme_manager.build_theme_path()
+
+            #  THEN: The thumbnail path should be a sub path of the test path
+            self.assertEqual(self.theme_manager.thumb_path, Path('test/path/thumbnails'))
+
     def test_click_on_new_theme(self):
         """
         Test the on_add_theme event handler is called by the UI
@@ -109,17 +127,16 @@
 
     @patch('openlp.core.ui.themeform.ThemeForm._setup')
     @patch('openlp.core.ui.filerenameform.FileRenameForm._setup')
-    def test_bootstrap_post(self, mocked_theme_form, mocked_rename_form):
+    def test_bootstrap_post(self, mocked_rename_form, mocked_theme_form):
         """
         Test the functions of bootstrap_post_setup are called.
         """
         # GIVEN:
         self.theme_manager.load_themes = MagicMock()
-        self.theme_manager.path = MagicMock()
+        self.theme_manager.theme_path = MagicMock()
 
         # WHEN:
         self.theme_manager.bootstrap_post_set_up()
 
         # THEN:
-        self.assertEqual(self.theme_manager.path, self.theme_manager.theme_form.path)
         self.assertEqual(1, self.theme_manager.load_themes.call_count, "load_themes should have been called once")


Follow ups