From 9cb2b2e3c2774a609a31a31efc503d5b628998f7 Mon Sep 17 00:00:00 2001 From: Philip Ridout Date: Tue, 5 Sep 2017 21:48:55 +0100 Subject: [PATCH 01/13] Pathlib changes --- openlp/core/__init__.py | 42 +++-- openlp/core/common/httputils.py | 46 +++--- openlp/core/lib/shutil.py | 97 +++++++++++ openlp/core/ui/firsttimeform.py | 6 +- openlp/plugins/remotes/deploy.py | 2 +- .../openlp_core_common/test_httputils.py | 3 +- .../functional/openlp_core_lib/test_shutil.py | 151 ++++++++++++++++++ 7 files changed, 294 insertions(+), 53 deletions(-) create mode 100755 openlp/core/lib/shutil.py create mode 100755 tests/functional/openlp_core_lib/test_shutil.py diff --git a/openlp/core/__init__.py b/openlp/core/__init__.py index 88b3dbfb7..0fcea2d1a 100644 --- a/openlp/core/__init__.py +++ b/openlp/core/__init__.py @@ -29,8 +29,6 @@ logging and a plugin framework are contained within the openlp.core module. import argparse import logging -import os -import shutil import sys import time from datetime import datetime @@ -43,6 +41,7 @@ from openlp.core.common import Registry, OpenLPMixin, AppLocation, LanguageManag from openlp.core.common.path import Path from openlp.core.common.versionchecker import VersionThread, get_application_version from openlp.core.lib import ScreenList +from openlp.core.lib.shutil import copytree from openlp.core.resources import qInitResources from openlp.core.ui import SplashScreen from openlp.core.ui.exceptionform import ExceptionForm @@ -181,25 +180,20 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication): """ Check if the data folder path exists. """ - data_folder_path = str(AppLocation.get_data_path()) - if not os.path.exists(data_folder_path): - log.critical('Database was not found in: ' + data_folder_path) - status = QtWidgets.QMessageBox.critical(None, translate('OpenLP', 'Data Directory Error'), - translate('OpenLP', 'OpenLP data folder was not found in:\n\n{path}' - '\n\nThe location of the data folder was ' - 'previously changed from the OpenLP\'s ' - 'default location. If the data was stored on ' - 'removable device, that device needs to be ' - 'made available.\n\nYou may reset the data ' - 'location back to the default location, ' - 'or you can try to make the current location ' - 'available.\n\nDo you want to reset to the ' - 'default data location? If not, OpenLP will be ' - 'closed so you can try to fix the the problem.') - .format(path=data_folder_path), - QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes | - QtWidgets.QMessageBox.No), - QtWidgets.QMessageBox.No) + data_folder_path = AppLocation.get_data_path() + if not data_folder_path.exists(): + log.critical('Database was not found in: %s', data_folder_path) + status = QtWidgets.QMessageBox.critical( + None, translate('OpenLP', 'Data Directory Error'), + translate('OpenLP', 'OpenLP data folder was not found in:\n\n{path}\n\nThe location of the data folder ' + 'was previously changed from the OpenLP\'s default location. If the data was ' + 'stored on removable device, that device needs to be made available.\n\nYou may ' + 'reset the data location back to the default location, or you can try to make the ' + 'current location available.\n\nDo you want to reset to the default data location? ' + 'If not, OpenLP will be closed so you can try to fix the the problem.') + .format(path=data_folder_path), + QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No), + QtWidgets.QMessageBox.No) if status == QtWidgets.QMessageBox.No: # If answer was "No", return "True", it will shutdown OpenLP in def main log.info('User requested termination') @@ -253,11 +247,11 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication): 'a backup of the old data folder?'), defaultButton=QtWidgets.QMessageBox.Yes) == QtWidgets.QMessageBox.Yes: # Create copy of data folder - data_folder_path = str(AppLocation.get_data_path()) + data_folder_path = AppLocation.get_data_path() timestamp = time.strftime("%Y%m%d-%H%M%S") - data_folder_backup_path = data_folder_path + '-' + timestamp + data_folder_backup_path = data_folder_path.with_name(data_folder_path.name + '-' + timestamp) try: - shutil.copytree(data_folder_path, data_folder_backup_path) + copytree(data_folder_path, data_folder_backup_path) except OSError: QtWidgets.QMessageBox.warning(None, translate('OpenLP', 'Backup'), translate('OpenLP', 'Backup of the data folder failed!')) diff --git a/openlp/core/common/httputils.py b/openlp/core/common/httputils.py index b0c9c1b2f..9f95f4924 100644 --- a/openlp/core/common/httputils.py +++ b/openlp/core/common/httputils.py @@ -211,38 +211,32 @@ def url_get_file(callback, url, f_path, sha256=None): :param callback: the class which needs to be updated :param url: URL to download - :param f_path: Destination file + :param openlp.core.common.path.Path f_path: Destination file :param sha256: The check sum value to be checked against the download value """ block_count = 0 block_size = 4096 retries = 0 log.debug("url_get_file: " + url) + if sha256: + hasher = hashlib.sha256() while True: try: - filename = open(f_path, "wb") - url_file = urllib.request.urlopen(url, timeout=CONNECTION_TIMEOUT) - if sha256: - hasher = hashlib.sha256() - # Download until finished or canceled. - while not callback.was_cancelled: - data = url_file.read(block_size) - if not data: - break - filename.write(data) - if sha256: - hasher.update(data) - block_count += 1 - callback._download_progress(block_count, block_size) - filename.close() - if sha256 and hasher.hexdigest() != sha256: - log.error('sha256 sums did not match for file: {file}'.format(file=f_path)) - os.remove(f_path) - return False - except (urllib.error.URLError, socket.timeout) as err: + with f_path.open('wb') as file: + url_file = urllib.request.urlopen(url, timeout=CONNECTION_TIMEOUT) + # Download until finished or canceled. + while not callback.was_cancelled: + data = url_file.read(block_size) + if not data: + break + file.write(data) + if sha256: + hasher.update(data) + block_count += 1 + callback._download_progress(block_count, block_size) + except (urllib.error.URLError, socket.timeout): trace_error_handler(log) - filename.close() - os.remove(f_path) + f_path.unlink() if retries > CONNECTION_RETRIES: return False else: @@ -251,8 +245,12 @@ def url_get_file(callback, url, f_path, sha256=None): continue break # Delete file if cancelled, it may be a partial file. + if sha256 and hasher.hexdigest() != sha256: + log.error('sha256 sums did not match for file: {file}'.format(file=f_path)) + f_path.unlink() + return False if callback.was_cancelled: - os.remove(f_path) + f_path.unlink() return True diff --git a/openlp/core/lib/shutil.py b/openlp/core/lib/shutil.py new file mode 100755 index 000000000..d2f6ae34d --- /dev/null +++ b/openlp/core/lib/shutil.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2017 OpenLP Developers # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" Patch the shutil methods we use so they accept and return Path objects""" +import shutil + +from openlp.core.common.path import path_to_str, str_to_path +from openlp.core.lib import replace_params + + +def copy(*args, **kwargs): + """ + Wraps :func:`shutil.copy` so that we can accept Path objects. + + :param src openlp.core.common.path.Path: Takes a Path object which is then converted to a str object + :param dst openlp.core.common.path.Path: Takes a Path object which is then converted to a str object + :return: Converts the str object received from :func:`shutil.copy` to a Path or NoneType object + :rtype: openlp.core.common.path.Path | None + + See the following link for more information on the other parameters: + https://docs.python.org/3/library/shutil.html#shutil.copy + """ + + args, kwargs = replace_params(args, kwargs, ((0, 'src', path_to_str), (1, 'dst', path_to_str))) + + return str_to_path(shutil.copy(*args, **kwargs)) + + +def copyfile(*args, **kwargs): + """ + Wraps :func:`shutil.copyfile` so that we can accept Path objects. + + :param openlp.core.common.path.Path src: Takes a Path object which is then converted to a str object + :param openlp.core.common.path.Path dst: Takes a Path object which is then converted to a str object + :return: Converts the str object received from :func:`shutil.copyfile` to a Path or NoneType object + :rtype: openlp.core.common.path.Path | None + + See the following link for more information on the other parameters: + https://docs.python.org/3/library/shutil.html#shutil.copyfile + """ + + args, kwargs = replace_params(args, kwargs, ((0, 'src', path_to_str), (1, 'dst', path_to_str))) + + return str_to_path(shutil.copyfile(*args, **kwargs)) + + +def copytree(*args, **kwargs): + """ + Wraps :func:shutil.copytree` so that we can accept Path objects. + + :param openlp.core.common.path.Path src : Takes a Path object which is then converted to a str object + :param openlp.core.common.path.Path dst: Takes a Path object which is then converted to a str object + :return: Converts the str object received from :func:`shutil.copytree` to a Path or NoneType object + :rtype: openlp.core.common.path.Path | None + + See the following link for more information on the other parameters: + https://docs.python.org/3/library/shutil.html#shutil.copytree + """ + + args, kwargs = replace_params(args, kwargs, ((0, 'src', path_to_str), (1, 'dst', path_to_str))) + + return str_to_path(shutil.copytree(*args, **kwargs)) + + +def rmtree(*args, **kwargs): + """ + Wraps :func:shutil.rmtree` so that we can accept Path objects. + + :param openlp.core.common.path.Path path: Takes a Path object which is then converted to a str object + :return: Passes the return from :func:`shutil.rmtree` back + :rtype: None + + See the following link for more information on the other parameters: + https://docs.python.org/3/library/shutil.html#shutil.rmtree + """ + + args, kwargs = replace_params(args, kwargs, ((0, 'path', path_to_str),)) + + return shutil.rmtree(*args, **kwargs) diff --git a/openlp/core/ui/firsttimeform.py b/openlp/core/ui/firsttimeform.py index 1ae923467..133d7d65b 100644 --- a/openlp/core/ui/firsttimeform.py +++ b/openlp/core/ui/firsttimeform.py @@ -563,7 +563,7 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): filename, sha256 = item.data(QtCore.Qt.UserRole) self._increment_progress_bar(self.downloading.format(name=filename), 0) self.previous_size = 0 - destination = os.path.join(songs_destination, str(filename)) + destination = Path(songs_destination, str(filename)) if not url_get_file(self, '{path}{name}'.format(path=self.songs_url, name=filename), destination, sha256): missed_files.append('Song: {name}'.format(name=filename)) @@ -576,7 +576,7 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): self._increment_progress_bar(self.downloading.format(name=bible), 0) self.previous_size = 0 if not url_get_file(self, '{path}{name}'.format(path=self.bibles_url, name=bible), - os.path.join(bibles_destination, bible), + Path(bibles_destination, bible), sha256): missed_files.append('Bible: {name}'.format(name=bible)) bibles_iterator += 1 @@ -588,7 +588,7 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): self._increment_progress_bar(self.downloading.format(name=theme), 0) self.previous_size = 0 if not url_get_file(self, '{path}{name}'.format(path=self.themes_url, name=theme), - os.path.join(themes_destination, theme), + Path(themes_destination, theme), sha256): missed_files.append('Theme: {name}'.format(name=theme)) if missed_files: diff --git a/openlp/plugins/remotes/deploy.py b/openlp/plugins/remotes/deploy.py index d971499f0..8706bc011 100644 --- a/openlp/plugins/remotes/deploy.py +++ b/openlp/plugins/remotes/deploy.py @@ -64,6 +64,6 @@ def download_and_check(callback=None): file_size = get_url_file_size('https://get.openlp.org/webclient/site.zip') callback.setRange(0, file_size) if url_get_file(callback, '{host}{name}'.format(host='https://get.openlp.org/webclient/', name='site.zip'), - os.path.join(str(AppLocation.get_section_data_path('remotes')), 'site.zip'), + AppLocation.get_section_data_path('remotes') / 'site.zip', sha256=sha256): deploy_zipfile(str(AppLocation.get_section_data_path('remotes')), 'site.zip') diff --git a/tests/functional/openlp_core_common/test_httputils.py b/tests/functional/openlp_core_common/test_httputils.py index 26ac297dd..03dde026c 100644 --- a/tests/functional/openlp_core_common/test_httputils.py +++ b/tests/functional/openlp_core_common/test_httputils.py @@ -29,6 +29,7 @@ from unittest import TestCase from unittest.mock import MagicMock, patch from openlp.core.common.httputils import get_user_agent, get_web_page, get_url_file_size, url_get_file, ping +from openlp.core.common.path import Path from tests.helpers.testmixin import TestMixin @@ -267,7 +268,7 @@ class TestHttpUtils(TestCase, TestMixin): mocked_urlopen.side_effect = socket.timeout() # WHEN: Attempt to retrieve a file - url_get_file(MagicMock(), url='http://localhost/test', f_path=self.tempfile) + url_get_file(MagicMock(), url='http://localhost/test', f_path=Path(self.tempfile)) # THEN: socket.timeout should have been caught # NOTE: Test is if $tmpdir/tempfile is still there, then test fails since ftw deletes bad downloaded files diff --git a/tests/functional/openlp_core_lib/test_shutil.py b/tests/functional/openlp_core_lib/test_shutil.py new file mode 100755 index 000000000..f502e403b --- /dev/null +++ b/tests/functional/openlp_core_lib/test_shutil.py @@ -0,0 +1,151 @@ +import os +from unittest import TestCase +from unittest.mock import ANY, MagicMock, patch + +from openlp.core.common.path import Path +from openlp.core.lib import shutilpatches + + +class TestShutilPatches(TestCase): + """ + Tests for the :mod:`openlp.core.lib.shutil` module + """ + + def test_pcopy(self): + """ + Test :func:`copy` + """ + # GIVEN: A mocked `shutil.copy` which returns a test path as a string + with patch('openlp.core.lib.shutil.shutil.copy', return_value=os.path.join('destination', 'test', 'path')) \ + as mocked_shutil_copy: + + # WHEN: Calling shutilpatches.copy with the src and dst parameters as Path object types + result = shutilpatches.copy(Path('source', 'test', 'path'), Path('destination', 'test', 'path')) + + # THEN: `shutil.copy` should have been called with the str equivalents of the Path objects. + # `shutilpatches.copy` should return the str type result of calling `shutil.copy` as a Path + # object. + mocked_shutil_copy.assert_called_once_with(os.path.join('source', 'test', 'path'), + os.path.join('destination', 'test', 'path')) + self.assertEqual(result, Path('destination', 'test', 'path')) + + def test_pcopy_follow_optional_params(self): + """ + Test :func:`copy` when follow_symlinks is set to false + """ + # GIVEN: A mocked `shutil.copy` + with patch('openlp.core.lib.shutil.shutil.copy', return_value='') as mocked_shutil_copy: + + # WHEN: Calling shutilpatches.copy with `follow_symlinks` set to False + shutilpatches.copy(Path('source', 'test', 'path'), + Path('destination', 'test', 'path'), + follow_symlinks=False) + + # THEN: `shutil.copy` should have been called with follow_symlinks is set to false + mocked_shutil_copy.assert_called_once_with(ANY, ANY, follow_symlinks=False) + + def test_pcopyfile(self): + """ + Test :func:`copyfile` + """ + # GIVEN: A mocked `shutil.copyfile` which returns a test path as a string + with patch('openlp.core.lib.shutil.shutil.copyfile', return_value=os.path.join('destination', 'test', 'path')) \ + as mocked_shutil_copyfile: + + # WHEN: Calling shutilpatches.copyfile with the src and dst parameters as Path object types + result = shutilpatches.copyfile(Path('source', 'test', 'path'), Path('destination', 'test', 'path')) + + # THEN: `shutil.copyfile` should have been called with the str equivalents of the Path objects. + # `shutilpatches.copyfile` should return the str type result of calling `shutil.copyfile` as a Path + # object. + mocked_shutil_copyfile.assert_called_once_with(os.path.join('source', 'test', 'path'), + os.path.join('destination', 'test', 'path')) + self.assertEqual(result, Path('destination', 'test', 'path')) + + def test_pcopyfile_optional_params(self): + """ + Test :func:`copyfile` when follow_symlinks is set to false + """ + # GIVEN: A mocked `shutil.copyfile` + with patch('openlp.core.lib.shutil.shutil.copyfile', return_value='') as mocked_shutil_copyfile: + + # WHEN: Calling shutilpatches.copyfile with `follow_symlinks` set to False + shutilpatches.copyfile(Path('source', 'test', 'path'), + Path('destination', 'test', 'path'), + follow_symlinks=False) + + # THEN: `shutil.copyfile` should have been called with the optional parameters, with out any of the values + # being modified + mocked_shutil_copyfile.assert_called_once_with(ANY, ANY, follow_symlinks=False) + + def test_pcopytree(self): + """ + Test :func:`copytree` + """ + # GIVEN: A mocked `shutil.copytree` which returns a test path as a string + with patch('openlp.core.lib.shutil.shutil.copytree', return_value=os.path.join('destination', 'test', 'path')) \ + as mocked_shutil_copytree: + + # WHEN: Calling shutilpatches.copytree with the src and dst parameters as Path object types + result = shutilpatches.copytree(Path('source', 'test', 'path'), Path('destination', 'test', 'path')) + + # THEN: `shutil.copytree` should have been called with the str equivalents of the Path objects. + # `shutilpatches.copytree` should return the str type result of calling `shutil.copytree` as a Path + # object. + mocked_shutil_copytree.assert_called_once_with(os.path.join('source', 'test', 'path'), + os.path.join('destination', 'test', 'path')) + self.assertEqual(result, Path('destination', 'test', 'path')) + + def test_pcopytree_optional_params(self): + """ + Test :func:`copytree` when optional parameters are passed + """ + # GIVEN: A mocked `shutil.copytree` + with patch('openlp.core.lib.shutil.shutil.copytree', return_value='') as mocked_shutil_copytree: + mocked_ignore = MagicMock() + mocked_copy_function = MagicMock() + + # WHEN: Calling shutilpatches.copytree with the optional parameters set + shutilpatches.copytree(Path('source', 'test', 'path'), + Path('destination', 'test', 'path'), + symlinks=True, + ignore=mocked_ignore, + copy_function=mocked_copy_function, + ignore_dangling_symlinks=True) + + # THEN: `shutil.copytree` should have been called with the optional parameters, with out any of the values + # being modified + mocked_shutil_copytree.assert_called_once_with(ANY, ANY, + symlinks=True, + ignore=mocked_ignore, + copy_function=mocked_copy_function, + ignore_dangling_symlinks=True) + + def test_prmtree(self): + """ + Test :func:`rmtree` + """ + # GIVEN: A mocked `shutil.rmtree` + with patch('openlp.core.lib.shutil.shutil.rmtree', return_value=None) as mocked_rmtree: + + # WHEN: Calling shutilpatches.rmtree with the path parameter as Path object type + result = shutilpatches.rmtree(Path('test', 'path')) + + # THEN: `shutil.rmtree` should have been called with the str equivalents of the Path object. + mocked_rmtree.assert_called_once_with(os.path.join('test', 'path')) + self.assertIsNone(result) + + def test_prmtree_optional_params(self): + """ + Test :func:`rmtree` when optional parameters are passed + """ + # GIVEN: A mocked `shutil.rmtree` + with patch('openlp.core.lib.shutil.shutil.rmtree', return_value='') as mocked_shutil_rmtree: + mocked_on_error = MagicMock() + + # WHEN: Calling shutilpatches.rmtree with `ignore_errors` set to True and `onerror` set to a mocked object + shutilpatches.rmtree(Path('test', 'path'), ignore_errors=True, onerror=mocked_on_error) + + # THEN: `shutil.rmtree` should have been called with the optional parameters, with out any of the values + # being modified + mocked_shutil_rmtree.assert_called_once_with(ANY, ignore_errors=True, onerror=mocked_on_error) From 292861907ef944e14bf620c52997b118cf9c056f Mon Sep 17 00:00:00 2001 From: Philip Ridout Date: Wed, 6 Sep 2017 21:18:08 +0100 Subject: [PATCH 02/13] minor edits --- openlp/core/common/applocation.py | 1 - openlp/core/common/httputils.py | 4 ++-- openlp/core/common/languagemanager.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/openlp/core/common/applocation.py b/openlp/core/common/applocation.py index 87ef7e6c1..02a872303 100644 --- a/openlp/core/common/applocation.py +++ b/openlp/core/common/applocation.py @@ -29,7 +29,6 @@ import sys from openlp.core.common import Settings, is_win, is_macosx from openlp.core.common.path import Path - if not is_win() and not is_macosx(): try: from xdg import BaseDirectory diff --git a/openlp/core/common/httputils.py b/openlp/core/common/httputils.py index 9f95f4924..90e128063 100644 --- a/openlp/core/common/httputils.py +++ b/openlp/core/common/httputils.py @@ -218,10 +218,10 @@ def url_get_file(callback, url, f_path, sha256=None): block_size = 4096 retries = 0 log.debug("url_get_file: " + url) - if sha256: - hasher = hashlib.sha256() while True: try: + if sha256: + hasher = hashlib.sha256() with f_path.open('wb') as file: url_file = urllib.request.urlopen(url, timeout=CONNECTION_TIMEOUT) # Download until finished or canceled. diff --git a/openlp/core/common/languagemanager.py b/openlp/core/common/languagemanager.py index 35b195031..40e4930fb 100644 --- a/openlp/core/common/languagemanager.py +++ b/openlp/core/common/languagemanager.py @@ -141,7 +141,7 @@ class LanguageManager(object): if reg_ex.exactMatch(qmf): name = '{regex}'.format(regex=reg_ex.cap(1)) LanguageManager.__qm_list__[ - '{count:>2i} {name}'.format(count=counter + 1, name=LanguageManager.language_name(qmf))] = name + '{count:>2d} {name}'.format(count=counter + 1, name=LanguageManager.language_name(qmf))] = name @staticmethod def get_qm_list(): From 24358337e7c3e25d378828e302fc61da1e92fbae Mon Sep 17 00:00:00 2001 From: Philip Ridout Date: Wed, 6 Sep 2017 22:36:31 +0100 Subject: [PATCH 03/13] Pathlib changes --- openlp/core/common/registry.py | 5 +- openlp/core/common/uistrings.py | 3 -- openlp/core/ui/servicemanager.py | 37 ++++++------- openlp/plugins/songs/reporting.py | 88 ++++++++++++++----------------- 4 files changed, 59 insertions(+), 74 deletions(-) diff --git a/openlp/core/common/registry.py b/openlp/core/common/registry.py index 95f1ce721..33afe6f21 100644 --- a/openlp/core/common/registry.py +++ b/openlp/core/common/registry.py @@ -138,12 +138,9 @@ class Registry(object): if result: results.append(result) except TypeError: - # Who has called me can help in debugging - trace_error_handler(log) log.exception('Exception for function {function}'.format(function=function)) else: - trace_error_handler(log) - log.error("Event {event} called but not registered".format(event=event)) + log.exception('Event {event} called but not registered'.format(event=event)) return results def get_flag(self, key): diff --git a/openlp/core/common/uistrings.py b/openlp/core/common/uistrings.py index 02937351d..9dd24a866 100644 --- a/openlp/core/common/uistrings.py +++ b/openlp/core/common/uistrings.py @@ -88,9 +88,6 @@ class UiStrings(object): self.Error = translate('OpenLP.Ui', 'Error') self.Export = translate('OpenLP.Ui', 'Export') self.File = translate('OpenLP.Ui', 'File') - self.FileNotFound = translate('OpenLP.Ui', 'File Not Found') - self.FileNotFoundMessage = translate('OpenLP.Ui', - 'File {name} not found.\nPlease try selecting it individually.') self.FontSizePtUnit = translate('OpenLP.Ui', 'pt', 'Abbreviated font pointsize unit') self.Help = translate('OpenLP.Ui', 'Help') self.Hours = translate('OpenLP.Ui', 'h', 'The abbreviated unit for hours') diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index 5051e43c1..eb279f267 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -366,16 +366,17 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa """ return self._modified - def set_file_name(self, file_name): + def set_file_name(self, file_path): """ Setter for service file. - :param file_name: The service file name + :param openlp.core.common.path.Path file_path: The service file name + :rtype: None """ - self._file_name = str(file_name) + self._file_name = path_to_str(file_path) self.main_window.set_service_modified(self.is_modified(), self.short_file_name()) - Settings().setValue('servicemanager/last file', Path(file_name)) - self._save_lite = self._file_name.endswith('.oszl') + Settings().setValue('servicemanager/last file', file_path) + self._save_lite = file_path.suffix() == '.oszl' def file_name(self): """ @@ -474,7 +475,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa """ self.service_manager_list.clear() self.service_items = [] - self.set_file_name('') + self.set_file_name(None) self.service_id += 1 self.set_modified(False) Settings().setValue('servicemanager/last file', None) @@ -695,27 +696,23 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa default_file_name = format_time(default_pattern, local_time) else: default_file_name = '' - directory = path_to_str(Settings().value(self.main_window.service_manager_settings_section + '/last directory')) - path = os.path.join(directory, default_file_name) + directory_path = Settings().value(self.main_window.service_manager_settings_section + '/last directory') + file_path = directory_path / default_file_name # SaveAs from osz to oszl is not valid as the files will be deleted on exit which is not sensible or usable in # the long term. if self._file_name.endswith('oszl') or self.service_has_all_original_files: - file_name, filter_used = QtWidgets.QFileDialog.getSaveFileName( - self.main_window, UiStrings().SaveService, path, + file_path, filter_used = FileDialog.getSaveFileName( + self.main_window, UiStrings().SaveService, file_path, translate('OpenLP.ServiceManager', 'OpenLP Service Files (*.osz);; OpenLP Service Files - lite (*.oszl)')) else: - file_name, filter_used = QtWidgets.QFileDialog.getSaveFileName( - self.main_window, UiStrings().SaveService, path, + file_path, filter_used = FileDialog.getSaveFileName( + self.main_window, UiStrings().SaveService, file_path, translate('OpenLP.ServiceManager', 'OpenLP Service Files (*.osz);;')) - if not file_name: + if not file_path: return False - if os.path.splitext(file_name)[1] == '': - file_name += '.osz' - else: - ext = os.path.splitext(file_name)[1] - file_name.replace(ext, '.osz') - self.set_file_name(file_name) + file_path.with_suffix('.osz') + self.set_file_name(file_path) self.decide_save_method() def decide_save_method(self, field=None): @@ -772,7 +769,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa return file_to.close() self.new_file() - self.set_file_name(file_name) + self.set_file_name(str_to_path(file_name)) self.main_window.display_progress_bar(len(items)) self.process_service_items(items) delete_file(Path(p_file)) diff --git a/openlp/plugins/songs/reporting.py b/openlp/plugins/songs/reporting.py index fc1a5f3f5..b50cd0a0c 100644 --- a/openlp/plugins/songs/reporting.py +++ b/openlp/plugins/songs/reporting.py @@ -25,10 +25,10 @@ The :mod:`db` module provides the ability to provide a csv file of all songs import csv import logging -from PyQt5 import QtWidgets - from openlp.core.common import Registry, translate +from openlp.core.common.path import Path from openlp.core.lib.ui import critical_error_message_box +from openlp.core.ui.lib.filedialog import FileDialog from openlp.plugins.songs.lib.db import Song @@ -42,58 +42,55 @@ def report_song_list(): """ main_window = Registry().get('main_window') plugin = Registry().get('songs').plugin - report_file_name, filter_used = QtWidgets.QFileDialog.getSaveFileName( + report_file_path, filter_used = FileDialog.getSaveFileName( main_window, translate('SongPlugin.ReportSongList', 'Save File'), - translate('SongPlugin.ReportSongList', 'song_extract.csv'), + Path(translate('SongPlugin.ReportSongList', 'song_extract.csv')), translate('SongPlugin.ReportSongList', 'CSV format (*.csv)')) - if not report_file_name: + if not report_file_path: main_window.error_message( translate('SongPlugin.ReportSongList', 'Output Path Not Selected'), - translate('SongPlugin.ReportSongList', 'You have not set a valid output location for your ' - 'report. \nPlease select an existing path ' - 'on your computer.') + translate('SongPlugin.ReportSongList', 'You have not set a valid output location for your report. \n' + 'Please select an existing path on your computer.') ) return - if not report_file_name.endswith('csv'): - report_file_name += '.csv' - file_handle = None + report_file_path.with_suffix('.csv') Registry().get('application').set_busy_cursor() try: - file_handle = open(report_file_name, 'wt') - fieldnames = ('Title', 'Alternative Title', 'Copyright', 'Author(s)', 'Song Book', 'Topic') - writer = csv.DictWriter(file_handle, fieldnames=fieldnames, quoting=csv.QUOTE_ALL) - headers = dict((n, n) for n in fieldnames) - writer.writerow(headers) - song_list = plugin.manager.get_all_objects(Song) - for song in song_list: - author_list = [] - for author_song in song.authors_songs: - author_list.append(author_song.author.display_name) - author_string = ' | '.join(author_list) - book_list = [] - for book_song in song.songbook_entries: - if hasattr(book_song, 'entry') and book_song.entry: - book_list.append('{name} #{entry}'.format(name=book_song.songbook.name, entry=book_song.entry)) - book_string = ' | '.join(book_list) - topic_list = [] - for topic_song in song.topics: - if hasattr(topic_song, 'name'): - topic_list.append(topic_song.name) - topic_string = ' | '.join(topic_list) - writer.writerow({'Title': song.title, - 'Alternative Title': song.alternate_title, - 'Copyright': song.copyright, - 'Author(s)': author_string, - 'Song Book': book_string, - 'Topic': topic_string}) - Registry().get('application').set_normal_cursor() - main_window.information_message( - translate('SongPlugin.ReportSongList', 'Report Creation'), - translate('SongPlugin.ReportSongList', - 'Report \n{name} \nhas been successfully created. ').format(name=report_file_name) - ) + with report_file_path.open('wt') as file_handle: + fieldnames = ('Title', 'Alternative Title', 'Copyright', 'Author(s)', 'Song Book', 'Topic') + writer = csv.DictWriter(file_handle, fieldnames=fieldnames, quoting=csv.QUOTE_ALL) + headers = dict((n, n) for n in fieldnames) + writer.writerow(headers) + song_list = plugin.manager.get_all_objects(Song) + for song in song_list: + author_list = [] + for author_song in song.authors_songs: + author_list.append(author_song.author.display_name) + author_string = ' | '.join(author_list) + book_list = [] + for book_song in song.songbook_entries: + if hasattr(book_song, 'entry') and book_song.entry: + book_list.append('{name} #{entry}'.format(name=book_song.songbook.name, entry=book_song.entry)) + book_string = ' | '.join(book_list) + topic_list = [] + for topic_song in song.topics: + if hasattr(topic_song, 'name'): + topic_list.append(topic_song.name) + topic_string = ' | '.join(topic_list) + writer.writerow({'Title': song.title, + 'Alternative Title': song.alternate_title, + 'Copyright': song.copyright, + 'Author(s)': author_string, + 'Song Book': book_string, + 'Topic': topic_string}) + Registry().get('application').set_normal_cursor() + main_window.information_message( + translate('SongPlugin.ReportSongList', 'Report Creation'), + translate('SongPlugin.ReportSongList', + 'Report \n{name} \nhas been successfully created. ').format(name=report_file_path) + ) except OSError as ose: Registry().get('application').set_normal_cursor() log.exception('Failed to write out song usage records') @@ -101,6 +98,3 @@ def report_song_list(): translate('SongPlugin.ReportSongList', 'An error occurred while extracting: {error}' ).format(error=ose.strerror)) - finally: - if file_handle: - file_handle.close() From f0e7381f5c107830f4d5a4164ca53206edb9436a Mon Sep 17 00:00:00 2001 From: Philip Ridout Date: Thu, 7 Sep 2017 22:52:39 +0100 Subject: [PATCH 04/13] Pathlib changes in presentation plugin --- openlp/core/ui/lib/wizard.py | 2 +- openlp/core/ui/mainwindow.py | 19 +++---- .../presentations/lib/impresscontroller.py | 15 +++--- openlp/plugins/presentations/lib/mediaitem.py | 14 +++--- .../presentations/lib/pdfcontroller.py | 34 ++++++------- .../presentations/lib/powerpointcontroller.py | 2 +- .../presentations/lib/pptviewcontroller.py | 12 ++--- .../lib/presentationcontroller.py | 50 +++++++++++-------- openlp/plugins/songs/reporting.py | 2 +- .../openlp_core_ui/test_exceptionform.py | 2 +- 10 files changed, 82 insertions(+), 70 deletions(-) diff --git a/openlp/core/ui/lib/wizard.py b/openlp/core/ui/lib/wizard.py index 8f3093fef..677949b33 100644 --- a/openlp/core/ui/lib/wizard.py +++ b/openlp/core/ui/lib/wizard.py @@ -310,7 +310,7 @@ class OpenLPWizard(QtWidgets.QWizard, RegistryProperties): """ folder_path = FileDialog.getExistingDirectory( self, title, Settings().value(self.plugin.settings_section + '/' + setting_name), - QtWidgets.QFileDialog.ShowDirsOnly) + FileDialog.ShowDirsOnly) if folder_path: editbox.setText(str(folder_path)) Settings().setValue(self.plugin.settings_section + '/' + setting_name, folder_path) diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 413e97a17..88799b060 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -42,6 +42,7 @@ 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.shutil import copyfile from openlp.core.lib.ui import create_action from openlp.core.ui import AboutForm, SettingsForm, ServiceManager, ThemeManager, LiveController, PluginForm, \ ShortcutListForm, FormattingTagForm, PreviewController @@ -848,12 +849,12 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): QtWidgets.QMessageBox.No) if answer == QtWidgets.QMessageBox.No: return - import_file_name, filter_used = QtWidgets.QFileDialog.getOpenFileName( + import_file_path, filter_used = FileDialog.getOpenFileName( self, translate('OpenLP.MainWindow', 'Import settings'), - '', + None, translate('OpenLP.MainWindow', 'OpenLP Settings (*.conf)')) - if not import_file_name: + if import_file_path is None: return setting_sections = [] # Add main sections. @@ -871,12 +872,12 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): # Add plugin sections. setting_sections.extend([plugin.name for plugin in self.plugin_manager.plugins]) # Copy the settings file to the tmp dir, because we do not want to change the original one. - temp_directory = os.path.join(str(gettempdir()), 'openlp') - check_directory_exists(Path(temp_directory)) - temp_config = os.path.join(temp_directory, os.path.basename(import_file_name)) - shutil.copyfile(import_file_name, temp_config) + temp_dir_path = Path(gettempdir(), 'openlp') + check_directory_exists(temp_dir_path) + temp_config_path = temp_dir_path / import_file_path.name + copyfile(import_file_path, temp_config_path) settings = Settings() - import_settings = Settings(temp_config, Settings.IniFormat) + import_settings = Settings(str(temp_config_path), Settings.IniFormat) log.info('hook upgrade_plugin_settings') self.plugin_manager.hook_upgrade_plugin_settings(import_settings) @@ -920,7 +921,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties): settings.setValue('{key}'.format(key=section_key), value) now = datetime.now() settings.beginGroup(self.header_section) - settings.setValue('file_imported', import_file_name) + settings.setValue('file_imported', import_file_path) settings.setValue('file_date_imported', now.strftime("%Y-%m-%d %H:%M")) settings.endGroup() settings.sync() diff --git a/openlp/plugins/presentations/lib/impresscontroller.py b/openlp/plugins/presentations/lib/impresscontroller.py index 25c470f48..7699946a0 100644 --- a/openlp/plugins/presentations/lib/impresscontroller.py +++ b/openlp/plugins/presentations/lib/impresscontroller.py @@ -255,10 +255,10 @@ class ImpressDocument(PresentationDocument): if self.check_thumbnails(): return if is_win(): - thumb_dir_url = 'file:///' + self.get_temp_folder().replace('\\', '/') \ + thumb_dir_url = 'file:///' + str(self.get_temp_folder()).replace('\\', '/') \ .replace(':', '|').replace(' ', '%20') else: - thumb_dir_url = uno.systemPathToFileUrl(self.get_temp_folder()) + thumb_dir_url = uno.systemPathToFileUrl(str(self.get_temp_folder())) properties = [] properties.append(self.create_property('FilterName', 'impress_png_Export')) properties = tuple(properties) @@ -266,17 +266,18 @@ class ImpressDocument(PresentationDocument): pages = doc.getDrawPages() if not pages: return - if not os.path.isdir(self.get_temp_folder()): - os.makedirs(self.get_temp_folder()) + temp_folder_path = self.get_temp_folder() + if not temp_folder_path.isdir(): + temp_folder_path.mkdir() for index in range(pages.getCount()): page = pages.getByIndex(index) doc.getCurrentController().setCurrentPage(page) url_path = '{path}/{name}.png'.format(path=thumb_dir_url, name=str(index + 1)) - path = os.path.join(self.get_temp_folder(), str(index + 1) + '.png') + path = temp_folder_path / '{number).png'.format(number=index + 1) try: doc.storeToURL(url_path, properties) - self.convert_thumbnail(path, index + 1) - delete_file(Path(path)) + self.convert_thumbnail(str(path), index + 1) + delete_file(path) except ErrorCodeIOException as exception: log.exception('ERROR! ErrorCodeIOException {error:d}'.format(error=exception.ErrCode)) except: diff --git a/openlp/plugins/presentations/lib/mediaitem.py b/openlp/plugins/presentations/lib/mediaitem.py index 99c937eb0..275279e15 100644 --- a/openlp/plugins/presentations/lib/mediaitem.py +++ b/openlp/plugins/presentations/lib/mediaitem.py @@ -187,7 +187,7 @@ class PresentationMediaItem(MediaManagerItem): if controller_name: controller = self.controllers[controller_name] doc = controller.add_document(file) - thumb = os.path.join(doc.get_thumbnail_folder(), 'icon.png') + thumb = str(doc.get_thumbnail_folder() / 'icon.png') preview = doc.get_thumbnail_path(1, True) if not preview and not initial_load: doc.load_presentation() @@ -304,17 +304,17 @@ class PresentationMediaItem(MediaManagerItem): controller = self.controllers[processor] service_item.processor = None doc = controller.add_document(filename) - if doc.get_thumbnail_path(1, True) is None or not os.path.isfile( - os.path.join(doc.get_temp_folder(), 'mainslide001.png')): + if doc.get_thumbnail_path(1, True) is None or \ + not (doc.get_temp_folder() / 'mainslide001.png').is_file(): doc.load_presentation() i = 1 - image = os.path.join(doc.get_temp_folder(), 'mainslide{number:0>3d}.png'.format(number=i)) - thumbnail = os.path.join(doc.get_thumbnail_folder(), 'slide%d.png' % i) + image = str(doc.get_temp_folder() / 'mainslide{number:0>3d}.png'.format(number=i)) + thumbnail = str(doc.get_thumbnail_folder() / 'slide{number:d}.png'.format(number=i)) while os.path.isfile(image): service_item.add_from_image(image, name, thumbnail=thumbnail) i += 1 - image = os.path.join(doc.get_temp_folder(), 'mainslide{number:0>3d}.png'.format(number=i)) - thumbnail = os.path.join(doc.get_thumbnail_folder(), 'slide{number:d}.png'.format(number=i)) + image = str(doc.get_temp_folder() / 'mainslide{number:0>3d}.png'.format(number=i)) + thumbnail = str(doc.get_thumbnail_folder() / 'slide{number:d}.png'.format(number=i)) service_item.add_capability(ItemCapabilities.HasThumbnails) doc.close_presentation() return True diff --git a/openlp/plugins/presentations/lib/pdfcontroller.py b/openlp/plugins/presentations/lib/pdfcontroller.py index f4a091551..f80ad94ba 100644 --- a/openlp/plugins/presentations/lib/pdfcontroller.py +++ b/openlp/plugins/presentations/lib/pdfcontroller.py @@ -240,46 +240,46 @@ class PdfDocument(PresentationDocument): :return: True is loading succeeded, otherwise False. """ log.debug('load_presentation pdf') + temp_dir_path = self.get_temp_folder() # Check if the images has already been created, and if yes load them - if os.path.isfile(os.path.join(self.get_temp_folder(), 'mainslide001.png')): - created_files = sorted(os.listdir(self.get_temp_folder())) - for fn in created_files: - if os.path.isfile(os.path.join(self.get_temp_folder(), fn)): - self.image_files.append(os.path.join(self.get_temp_folder(), fn)) + if (temp_dir_path / 'mainslide001.png').is_file(): + created_files = sorted(temp_dir_path.glob('*')) + for image_path in created_files: + if image_path.is_file(): + self.image_files.append(str(image_path)) self.num_pages = len(self.image_files) return True size = ScreenList().current['size'] # Generate images from PDF that will fit the frame. runlog = '' try: - if not os.path.isdir(self.get_temp_folder()): - os.makedirs(self.get_temp_folder()) + if not temp_dir_path.is_dir(): + temp_dir_path.mkdir(parents=True) # The %03d in the file name is handled by each binary if self.controller.mudrawbin: log.debug('loading presentation using mudraw') runlog = check_output([self.controller.mudrawbin, '-w', str(size.width()), '-h', str(size.height()), - '-o', os.path.join(self.get_temp_folder(), 'mainslide%03d.png'), self.file_path], + '-o', str(temp_dir_path / 'mainslide%03d.png'), self.file_path], startupinfo=self.startupinfo) elif self.controller.mutoolbin: log.debug('loading presentation using mutool') runlog = check_output([self.controller.mutoolbin, 'draw', '-w', str(size.width()), '-h', str(size.height()), - '-o', os.path.join(self.get_temp_folder(), 'mainslide%03d.png'), self.file_path], + '-o', str(temp_dir_path / 'mainslide%03d.png'), self.file_path], startupinfo=self.startupinfo) elif self.controller.gsbin: log.debug('loading presentation using gs') resolution = self.gs_get_resolution(size) runlog = check_output([self.controller.gsbin, '-dSAFER', '-dNOPAUSE', '-dBATCH', '-sDEVICE=png16m', '-r' + str(resolution), '-dTextAlphaBits=4', '-dGraphicsAlphaBits=4', - '-sOutputFile=' + os.path.join(self.get_temp_folder(), 'mainslide%03d.png'), + '-sOutputFile=' + str(temp_dir_path / 'mainslide%03d.png'), self.file_path], startupinfo=self.startupinfo) - created_files = sorted(os.listdir(self.get_temp_folder())) - for fn in created_files: - if os.path.isfile(os.path.join(self.get_temp_folder(), fn)): - self.image_files.append(os.path.join(self.get_temp_folder(), fn)) - except Exception as e: - log.debug(e) - log.debug(runlog) + created_files = sorted(temp_dir_path.glob('*')) + for image_path in created_files: + if image_path.is_file(): + self.image_files.append(str(image_path)) + except Exception: + log.exception(runlog) return False self.num_pages = len(self.image_files) # Create thumbnails diff --git a/openlp/plugins/presentations/lib/powerpointcontroller.py b/openlp/plugins/presentations/lib/powerpointcontroller.py index 3bd726027..1014db851 100644 --- a/openlp/plugins/presentations/lib/powerpointcontroller.py +++ b/openlp/plugins/presentations/lib/powerpointcontroller.py @@ -177,7 +177,7 @@ class PowerpointDocument(PresentationDocument): if not self.presentation.Slides(num + 1).SlideShowTransition.Hidden: self.index_map[key] = num + 1 self.presentation.Slides(num + 1).Export( - os.path.join(self.get_thumbnail_folder(), 'slide{key:d}.png'.format(key=key)), 'png', 320, 240) + str(self.get_thumbnail_folder() / 'slide{key:d}.png'.format(key=key)), 'png', 320, 240) key += 1 self.slide_count = key - 1 diff --git a/openlp/plugins/presentations/lib/pptviewcontroller.py b/openlp/plugins/presentations/lib/pptviewcontroller.py index 645bef053..c936fe65c 100644 --- a/openlp/plugins/presentations/lib/pptviewcontroller.py +++ b/openlp/plugins/presentations/lib/pptviewcontroller.py @@ -121,17 +121,17 @@ class PptviewDocument(PresentationDocument): the background PptView task started earlier. """ log.debug('LoadPresentation') - temp_folder = self.get_temp_folder() + temp_dir_path = self.get_temp_folder() size = ScreenList().current['size'] rect = RECT(size.x(), size.y(), size.right(), size.bottom()) self.file_path = os.path.normpath(self.file_path) - preview_path = os.path.join(temp_folder, 'slide') + preview_path = temp_dir_path / 'slide' # Ensure that the paths are null terminated byte_file_path = self.file_path.encode('utf-16-le') + b'\0' - preview_path = preview_path.encode('utf-16-le') + b'\0' - if not os.path.isdir(temp_folder): - os.makedirs(temp_folder) - self.ppt_id = self.controller.process.OpenPPT(byte_file_path, None, rect, preview_path) + preview_file_name = str(preview_path).encode('utf-16-le') + b'\0' + if not temp_dir_path: + temp_dir_path.mkdir(parents=True) + self.ppt_id = self.controller.process.OpenPPT(byte_file_path, None, rect, preview_file_name) if self.ppt_id >= 0: self.create_thumbnails() self.stop_presentation() diff --git a/openlp/plugins/presentations/lib/presentationcontroller.py b/openlp/plugins/presentations/lib/presentationcontroller.py index e5f8ccf21..af665bb55 100644 --- a/openlp/plugins/presentations/lib/presentationcontroller.py +++ b/openlp/plugins/presentations/lib/presentationcontroller.py @@ -29,6 +29,7 @@ from PyQt5 import QtCore from openlp.core.common import Registry, AppLocation, Settings, check_directory_exists, md5_hash from openlp.core.common.path import Path from openlp.core.lib import create_thumb, validate_thumb +from openlp.core.lib.shutil import rmtree log = logging.getLogger(__name__) @@ -99,7 +100,7 @@ class PresentationDocument(object): """ self.slide_number = 0 self.file_path = name - check_directory_exists(Path(self.get_thumbnail_folder())) + check_directory_exists(self.get_thumbnail_folder()) def load_presentation(self): """ @@ -116,10 +117,12 @@ class PresentationDocument(object): a file, e.g. thumbnails """ try: - if os.path.exists(self.get_thumbnail_folder()): - shutil.rmtree(self.get_thumbnail_folder()) - if os.path.exists(self.get_temp_folder()): - shutil.rmtree(self.get_temp_folder()) + thumbnail_folder_path = self.get_thumbnail_folder() + temp_folder_path = self.get_temp_folder() + if thumbnail_folder_path.exists(): + rmtree(thumbnail_folder_path) + if temp_folder_path.exists(): + rmtree(temp_folder_path) except OSError: log.exception('Failed to delete presentation controller files') @@ -132,24 +135,30 @@ class PresentationDocument(object): def get_thumbnail_folder(self): """ The location where thumbnail images will be stored + + :return: The path to the thumbnail + :rtype: openlp.core.common.path.Path """ # TODO: If statement can be removed when the upgrade path from 2.0.x to 2.2.x is no longer needed if Settings().value('presentations/thumbnail_scheme') == 'md5': folder = md5_hash(self.file_path.encode('utf-8')) else: folder = self.get_file_name() - return os.path.join(self.controller.thumbnail_folder, folder) + return Path(self.controller.thumbnail_folder, folder) def get_temp_folder(self): """ The location where thumbnail images will be stored + + :return: The path to the temporary file folder + :rtype: openlp.core.common.path.Path """ # TODO: If statement can be removed when the upgrade path from 2.0.x to 2.2.x is no longer needed if Settings().value('presentations/thumbnail_scheme') == 'md5': folder = md5_hash(self.file_path.encode('utf-8')) else: - folder = folder = self.get_file_name() - return os.path.join(self.controller.temp_folder, folder) + folder = self.get_file_name() + return Path(self.controller.temp_folder, folder) def check_thumbnails(self): """ @@ -251,15 +260,17 @@ class PresentationDocument(object): thumb_path = self.get_thumbnail_path(idx, False) create_thumb(file, thumb_path, False, QtCore.QSize(-1, 360)) - def get_thumbnail_path(self, slide_no, check_exists): + def get_thumbnail_path(self, slide_no, check_exists=True): """ Returns an image path containing a preview for the requested slide - :param slide_no: The slide an image is required for, starting at 1 - :param check_exists: + :param int slide_no: The slide an image is required for, starting at 1 + :param bool check_exists: Check if the generated path exists + :return: The path, or None if the :param:`check_exists` is True and the file does not exist + :rtype: openlp.core.common.path.Path, None """ - path = os.path.join(self.get_thumbnail_folder(), self.controller.thumbnail_prefix + str(slide_no) + '.png') - if os.path.isfile(path) or not check_exists: + path = self.get_thumbnail_folder() / (self.controller.thumbnail_prefix + str(slide_no) + '.png') + if path.is_file() or not check_exists: return path else: return None @@ -304,7 +315,7 @@ class PresentationDocument(object): """ titles = [] notes = [] - titles_file = os.path.join(self.get_thumbnail_folder(), 'titles.txt') + titles_file = str(self.get_thumbnail_folder() / 'titles.txt') if os.path.exists(titles_file): try: with open(titles_file, encoding='utf-8') as fi: @@ -313,7 +324,7 @@ class PresentationDocument(object): log.exception('Failed to open/read existing titles file') titles = [] for slide_no, title in enumerate(titles, 1): - notes_file = os.path.join(self.get_thumbnail_folder(), 'slideNotes{number:d}.txt'.format(number=slide_no)) + notes_file = str(self.get_thumbnail_folder() / 'slideNotes{number:d}.txt'.format(number=slide_no)) note = '' if os.path.exists(notes_file): try: @@ -331,14 +342,13 @@ class PresentationDocument(object): and notes to the slideNote%.txt """ if titles: - titles_file = os.path.join(self.get_thumbnail_folder(), 'titles.txt') - with open(titles_file, mode='wt', encoding='utf-8') as fo: + titles_path = self.get_thumbnail_folder() / 'titles.txt' + with titles_path.open(mode='wt', encoding='utf-8') as fo: fo.writelines(titles) if notes: for slide_no, note in enumerate(notes, 1): - notes_file = os.path.join(self.get_thumbnail_folder(), - 'slideNotes{number:d}.txt'.format(number=slide_no)) - with open(notes_file, mode='wt', encoding='utf-8') as fn: + notes_path = self.get_thumbnail_folder() / 'slideNotes{number:d}.txt'.format(number=slide_no) + with notes_path.open(mode='wt', encoding='utf-8') as fn: fn.write(note) diff --git a/openlp/plugins/songs/reporting.py b/openlp/plugins/songs/reporting.py index b50cd0a0c..066a0ea26 100644 --- a/openlp/plugins/songs/reporting.py +++ b/openlp/plugins/songs/reporting.py @@ -48,7 +48,7 @@ def report_song_list(): Path(translate('SongPlugin.ReportSongList', 'song_extract.csv')), translate('SongPlugin.ReportSongList', 'CSV format (*.csv)')) - if not report_file_path: + if report_file_path is None: main_window.error_message( translate('SongPlugin.ReportSongList', 'Output Path Not Selected'), translate('SongPlugin.ReportSongList', 'You have not set a valid output location for your report. \n' diff --git a/tests/functional/openlp_core_ui/test_exceptionform.py b/tests/functional/openlp_core_ui/test_exceptionform.py index 37a040aa6..40eb19ac8 100644 --- a/tests/functional/openlp_core_ui/test_exceptionform.py +++ b/tests/functional/openlp_core_ui/test_exceptionform.py @@ -103,7 +103,7 @@ class TestExceptionForm(TestMixin, TestCase): os.remove(self.tempfile) @patch("openlp.core.ui.exceptionform.Ui_ExceptionDialog") - @patch("openlp.core.ui.exceptionform.QtWidgets.QFileDialog") + @patch("openlp.core.ui.exceptionform.FileDialog") @patch("openlp.core.ui.exceptionform.QtCore.QUrl") @patch("openlp.core.ui.exceptionform.QtCore.QUrlQuery.addQueryItem") @patch("openlp.core.ui.exceptionform.Qt") From 8ed5903ced06462290dac9f959230a1ade566138 Mon Sep 17 00:00:00 2001 From: Philip Ridout Date: Fri, 15 Sep 2017 20:01:09 +0100 Subject: [PATCH 05/13] Moved most of the presentation plugin over to pathlib --- openlp/core/lib/__init__.py | 20 ++- openlp/core/lib/mediamanageritem.py | 2 - openlp/core/lib/shutil.py | 17 ++ .../presentations/lib/impresscontroller.py | 45 +++-- openlp/plugins/presentations/lib/mediaitem.py | 156 +++++++++--------- .../presentations/lib/messagelistener.py | 29 ++-- .../presentations/lib/pdfcontroller.py | 83 +++++----- .../presentations/lib/powerpointcontroller.py | 12 +- .../presentations/lib/pptviewcontroller.py | 35 ++-- .../lib/presentationcontroller.py | 119 ++++++------- .../presentations/lib/presentationtab.py | 9 +- .../test_presentationcontroller.py | 17 -- 12 files changed, 278 insertions(+), 266 deletions(-) diff --git a/openlp/core/lib/__init__.py b/openlp/core/lib/__init__.py index 08b675000..0babbc0d1 100644 --- a/openlp/core/lib/__init__.py +++ b/openlp/core/lib/__init__.py @@ -29,9 +29,10 @@ import os import re import math -from PyQt5 import QtCore, QtGui, Qt, QtWidgets +from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import translate +from openlp.core.common.path import Path log = logging.getLogger(__name__ + '.__init__') @@ -125,10 +126,11 @@ def build_icon(icon): Build a QIcon instance from an existing QIcon, a resource location, or a physical file location. If the icon is a QIcon instance, that icon is simply returned. If not, it builds a QIcon instance from the resource or file name. - :param icon: - The icon to build. This can be a QIcon, a resource string in the form ``:/resource/file.png``, or a file - location like ``/path/to/file.png``. However, the **recommended** way is to specify a resource string. + :param QtGui.QIcon | Path | QtGui.QIcon | str icon: + The icon to build. This can be a QIcon, a resource string in the form ``:/resource/file.png``, or a file path + location like ``Path(/path/to/file.png)``. However, the **recommended** way is to specify a resource string. :return: The build icon. + :rtype: QtGui.QIcon """ if isinstance(icon, QtGui.QIcon): return icon @@ -136,6 +138,8 @@ def build_icon(icon): button_icon = QtGui.QIcon() if isinstance(icon, str): pix_map = QtGui.QPixmap(icon) + elif isinstance(icon, Path): + pix_map = QtGui.QPixmap(str(icon)) elif isinstance(icon, QtGui.QImage): pix_map = QtGui.QPixmap.fromImage(icon) if pix_map: @@ -221,10 +225,12 @@ def validate_thumb(file_path, thumb_path): :param thumb_path: The path to the thumb. :return: True, False if the image has changed since the thumb was created. """ - if not os.path.exists(thumb_path): + file_path = Path(file_path) + thumb_path = Path(thumb_path) + if not thumb_path.exists(): return False - image_date = os.stat(file_path).st_mtime - thumb_date = os.stat(thumb_path).st_mtime + image_date = file_path.stat().st_mtime + thumb_date = thumb_path.stat().st_mtime return image_date <= thumb_date diff --git a/openlp/core/lib/mediamanageritem.py b/openlp/core/lib/mediamanageritem.py index 59a0e4e33..1c7a5b4ef 100644 --- a/openlp/core/lib/mediamanageritem.py +++ b/openlp/core/lib/mediamanageritem.py @@ -359,10 +359,8 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties): :param files: The files to be loaded. :param target_group: The QTreeWidgetItem of the group that will be the parent of the added files """ - names = [] full_list = [] for count in range(self.list_view.count()): - names.append(self.list_view.item(count).text()) full_list.append(self.list_view.item(count).data(QtCore.Qt.UserRole)) duplicates_found = False files_added = False diff --git a/openlp/core/lib/shutil.py b/openlp/core/lib/shutil.py index d2f6ae34d..44dea590a 100755 --- a/openlp/core/lib/shutil.py +++ b/openlp/core/lib/shutil.py @@ -95,3 +95,20 @@ def rmtree(*args, **kwargs): args, kwargs = replace_params(args, kwargs, ((0, 'path', path_to_str),)) return shutil.rmtree(*args, **kwargs) +# TODO: Test and tidy +def which(*args, **kwargs): + """ + Wraps :func:shutil.rmtree` so that we can accept Path objects. + + :param openlp.core.common.path.Path path: Takes a Path object which is then converted to a str object + :return: Passes the return from :func:`shutil.rmtree` back + :rtype: None + + See the following link for more information on the other parameters: + https://docs.python.org/3/library/shutil.html#shutil.rmtree + """ + + file_name = shutil.which(*args, **kwargs) + if file_name: + return str_to_path(file_name) + return None diff --git a/openlp/plugins/presentations/lib/impresscontroller.py b/openlp/plugins/presentations/lib/impresscontroller.py index 7699946a0..472e07801 100644 --- a/openlp/plugins/presentations/lib/impresscontroller.py +++ b/openlp/plugins/presentations/lib/impresscontroller.py @@ -32,11 +32,14 @@ # http://nxsy.org/comparing-documents-with-openoffice-and-python import logging -import os import time -from openlp.core.common import is_win, Registry, delete_file -from openlp.core.common.path import Path +from PyQt5 import QtCore + +from openlp.core.common import Registry, delete_file, get_uno_command, get_uno_instance, is_win +from openlp.core.lib import ScreenList +from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument, \ + TextType if is_win(): from win32com.client import Dispatch @@ -55,14 +58,6 @@ else: except ImportError: uno_available = False -from PyQt5 import QtCore - -from openlp.core.lib import ScreenList -from openlp.core.common import get_uno_command, get_uno_instance -from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument, \ - TextType - - log = logging.getLogger(__name__) @@ -203,12 +198,15 @@ class ImpressDocument(PresentationDocument): Class which holds information and controls a single presentation. """ - def __init__(self, controller, presentation): + def __init__(self, controller, document_path): """ Constructor, store information about the file and initialise. + + :param openlp.core.common.path.Path document_path: File path for the document to load + :rtype: None """ log.debug('Init Presentation OpenOffice') - super(ImpressDocument, self).__init__(controller, presentation) + super().__init__(controller, document_path) self.document = None self.presentation = None self.control = None @@ -225,10 +223,10 @@ class ImpressDocument(PresentationDocument): if desktop is None: self.controller.start_process() desktop = self.controller.get_com_desktop() - url = 'file:///' + self.file_path.replace('\\', '/').replace(':', '|').replace(' ', '%20') + url = self.file_path.as_uri() else: desktop = self.controller.get_uno_desktop() - url = uno.systemPathToFileUrl(self.file_path) + url = uno.systemPathToFileUrl(str(self.file_path)) if desktop is None: return False self.desktop = desktop @@ -254,11 +252,11 @@ class ImpressDocument(PresentationDocument): log.debug('create thumbnails OpenOffice') if self.check_thumbnails(): return + temp_folder_path = self.get_temp_folder() if is_win(): - thumb_dir_url = 'file:///' + str(self.get_temp_folder()).replace('\\', '/') \ - .replace(':', '|').replace(' ', '%20') + thumb_dir_url = temp_folder_path.as_uri() else: - thumb_dir_url = uno.systemPathToFileUrl(str(self.get_temp_folder())) + thumb_dir_url = uno.systemPathToFileUrl(str(temp_folder_path)) properties = [] properties.append(self.create_property('FilterName', 'impress_png_Export')) properties = tuple(properties) @@ -266,17 +264,16 @@ class ImpressDocument(PresentationDocument): pages = doc.getDrawPages() if not pages: return - temp_folder_path = self.get_temp_folder() - if not temp_folder_path.isdir(): - temp_folder_path.mkdir() + if not temp_folder_path.is_dir(): + temp_folder_path.mkdir(parents=True) for index in range(pages.getCount()): page = pages.getByIndex(index) doc.getCurrentController().setCurrentPage(page) - url_path = '{path}/{name}.png'.format(path=thumb_dir_url, name=str(index + 1)) - path = temp_folder_path / '{number).png'.format(number=index + 1) + url_path = '{path}/{name:d}.png'.format(path=thumb_dir_url, name=index + 1) + path = temp_folder_path / '{number:d}.png'.format(number=index + 1) try: doc.storeToURL(url_path, properties) - self.convert_thumbnail(str(path), index + 1) + self.convert_thumbnail(path, index + 1) delete_file(path) except ErrorCodeIOException as exception: log.exception('ERROR! ErrorCodeIOException {error:d}'.format(error=exception.ErrCode)) diff --git a/openlp/plugins/presentations/lib/mediaitem.py b/openlp/plugins/presentations/lib/mediaitem.py index 275279e15..aa5bfc0d6 100644 --- a/openlp/plugins/presentations/lib/mediaitem.py +++ b/openlp/plugins/presentations/lib/mediaitem.py @@ -19,15 +19,13 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### - import logging -import os from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import Registry, Settings, UiStrings, translate from openlp.core.common.languagemanager import get_locale_key -from openlp.core.common.path import path_to_str +from openlp.core.common.path import Path, path_to_str, str_to_path from openlp.core.lib import MediaManagerItem, ItemCapabilities, ServiceItemContext,\ build_icon, check_item_selected, create_thumb, validate_thumb from openlp.core.lib.ui import critical_error_message_box, create_horizontal_adjusting_combo_box @@ -128,7 +126,7 @@ class PresentationMediaItem(MediaManagerItem): """ self.list_view.setIconSize(QtCore.QSize(88, 50)) file_paths = Settings().value(self.settings_section + '/presentations files') - self.load_list([path_to_str(file) for file in file_paths], initial_load=True) + self.load_list([path_to_str(path) for path in file_paths], initial_load=True) self.populate_display_types() def populate_display_types(self): @@ -152,54 +150,57 @@ class PresentationMediaItem(MediaManagerItem): else: self.presentation_widget.hide() - def load_list(self, files, target_group=None, initial_load=False): + def load_list(self, file_paths, target_group=None, initial_load=False): """ Add presentations into the media manager. This is called both on initial load of the plugin to populate with existing files, and when the user adds new files via the media manager. + + :param list[openlp.core.common.path.Path] file_paths: List of file paths to add to the media manager. """ - current_list = self.get_file_list() - titles = [file_path.name for file_path in current_list] + file_paths = [str_to_path(filename) for filename in file_paths] + current_paths = self.get_file_list() + titles = [file_path.name for file_path in current_paths] self.application.set_busy_cursor() if not initial_load: - self.main_window.display_progress_bar(len(files)) + self.main_window.display_progress_bar(len(file_paths)) # Sort the presentations by its filename considering language specific characters. - files.sort(key=lambda filename: get_locale_key(os.path.split(str(filename))[1])) - for file in files: + file_paths.sort(key=lambda file_path: get_locale_key(file_path.name)) + for file_path in file_paths: if not initial_load: self.main_window.increment_progress_bar() - if current_list.count(file) > 0: + if current_paths.count(file_path) > 0: continue - filename = os.path.split(file)[1] - if not os.path.exists(file): - item_name = QtWidgets.QListWidgetItem(filename) + file_name = file_path.name + if not file_path.exists(): + item_name = QtWidgets.QListWidgetItem(file_name) item_name.setIcon(build_icon(ERROR_IMAGE)) - item_name.setData(QtCore.Qt.UserRole, file) - item_name.setToolTip(file) + item_name.setData(QtCore.Qt.UserRole, path_to_str(file_path)) + item_name.setToolTip(str(file_path)) self.list_view.addItem(item_name) else: - if titles.count(filename) > 0: + if titles.count(file_name) > 0: if not initial_load: critical_error_message_box(translate('PresentationPlugin.MediaItem', 'File Exists'), translate('PresentationPlugin.MediaItem', 'A presentation with that filename already exists.')) continue - controller_name = self.find_controller_by_type(filename) + controller_name = self.find_controller_by_type(file_path) if controller_name: controller = self.controllers[controller_name] - doc = controller.add_document(file) - thumb = str(doc.get_thumbnail_folder() / 'icon.png') - preview = doc.get_thumbnail_path(1, True) - if not preview and not initial_load: + doc = controller.add_document(file_path) + thumbnail_path = doc.get_thumbnail_folder() / 'icon.png' + preview_path = doc.get_thumbnail_path(1, True) + if not preview_path and not initial_load: doc.load_presentation() - preview = doc.get_thumbnail_path(1, True) + preview_path = doc.get_thumbnail_path(1, True) doc.close_presentation() - if not (preview and os.path.exists(preview)): + if not (preview_path and preview_path.exists()): icon = build_icon(':/general/general_delete.png') else: - if validate_thumb(preview, thumb): - icon = build_icon(thumb) + if validate_thumb(preview_path, thumbnail_path): + icon = build_icon(thumbnail_path) else: - icon = create_thumb(preview, thumb) + icon = create_thumb(str(preview_path), str(thumbnail_path)) else: if initial_load: icon = build_icon(':/general/general_delete.png') @@ -208,10 +209,10 @@ class PresentationMediaItem(MediaManagerItem): translate('PresentationPlugin.MediaItem', 'This type of presentation is not supported.')) continue - item_name = QtWidgets.QListWidgetItem(filename) - item_name.setData(QtCore.Qt.UserRole, file) + item_name = QtWidgets.QListWidgetItem(file_name) + item_name.setData(QtCore.Qt.UserRole, path_to_str(file_path)) item_name.setIcon(icon) - item_name.setToolTip(file) + item_name.setToolTip(str(file_path)) self.list_view.addItem(item_name) if not initial_load: self.main_window.finished_progress_bar() @@ -228,8 +229,8 @@ class PresentationMediaItem(MediaManagerItem): self.application.set_busy_cursor() self.main_window.display_progress_bar(len(row_list)) for item in items: - filepath = str(item.data(QtCore.Qt.UserRole)) - self.clean_up_thumbnails(filepath) + file_path = str_to_path(item.data(QtCore.Qt.UserRole)) + self.clean_up_thumbnails(file_path) self.main_window.increment_progress_bar() self.main_window.finished_progress_bar() for row in row_list: @@ -237,30 +238,29 @@ class PresentationMediaItem(MediaManagerItem): Settings().setValue(self.settings_section + '/presentations files', self.get_file_list()) self.application.set_normal_cursor() - def clean_up_thumbnails(self, filepath, clean_for_update=False): + def clean_up_thumbnails(self, file_path, clean_for_update=False): """ Clean up the files created such as thumbnails - :param filepath: File path of the presention to clean up after - :param clean_for_update: Only clean thumbnails if update is needed - :return: None + :param openlp.core.common.path.Path file_path: File path of the presention to clean up after + :param bool clean_for_update: Only clean thumbnails if update is needed + :rtype: None """ for cidx in self.controllers: - root, file_ext = os.path.splitext(filepath) - file_ext = file_ext[1:] + file_ext = file_path.suffix[1:] if file_ext in self.controllers[cidx].supports or file_ext in self.controllers[cidx].also_supports: - doc = self.controllers[cidx].add_document(filepath) + doc = self.controllers[cidx].add_document(file_path) if clean_for_update: thumb_path = doc.get_thumbnail_path(1, True) - if not thumb_path or not os.path.exists(filepath) or os.path.getmtime( - thumb_path) < os.path.getmtime(filepath): + if not thumb_path or not file_path.exists() or \ + thumb_path.stat().st_mtime < file_path.stat().st_mtime: doc.presentation_deleted() else: doc.presentation_deleted() doc.close_presentation() def generate_slide_data(self, service_item, item=None, xml_version=False, remote=False, - context=ServiceItemContext.Service, presentation_file=None): + context=ServiceItemContext.Service, file_path=None): """ Generate the slide data. Needs to be implemented by the plugin. @@ -276,10 +276,9 @@ class PresentationMediaItem(MediaManagerItem): items = self.list_view.selectedItems() if len(items) > 1: return False - filename = presentation_file - if filename is None: - filename = items[0].data(QtCore.Qt.UserRole) - file_type = os.path.splitext(filename.lower())[1][1:] + if file_path is None: + file_path = str_to_path(items[0].data(QtCore.Qt.UserRole)) + file_type = file_path.suffix.lower()[1:] if not self.display_type_combo_box.currentText(): return False service_item.add_capability(ItemCapabilities.CanEditTitle) @@ -292,29 +291,28 @@ class PresentationMediaItem(MediaManagerItem): # force a nonexistent theme service_item.theme = -1 for bitem in items: - filename = presentation_file - if filename is None: - filename = bitem.data(QtCore.Qt.UserRole) - (path, name) = os.path.split(filename) - service_item.title = name - if os.path.exists(filename): - processor = self.find_controller_by_type(filename) + if file_path is None: + file_path = str_to_path(bitem.data(QtCore.Qt.UserRole)) + path, file_name = file_path.parent, file_path.name + service_item.title = file_name + if file_path.exists(): + processor = self.find_controller_by_type(file_path) if not processor: return False controller = self.controllers[processor] service_item.processor = None - doc = controller.add_document(filename) + doc = controller.add_document(file_path) if doc.get_thumbnail_path(1, True) is None or \ - not (doc.get_temp_folder() / 'mainslide001.png').is_file(): + not (doc.get_temp_folder() / 'mainslide001.png').is_file(): doc.load_presentation() i = 1 - image = str(doc.get_temp_folder() / 'mainslide{number:0>3d}.png'.format(number=i)) - thumbnail = str(doc.get_thumbnail_folder() / 'slide{number:d}.png'.format(number=i)) - while os.path.isfile(image): - service_item.add_from_image(image, name, thumbnail=thumbnail) + image_path = doc.get_temp_folder() / 'mainslide{number:0>3d}.png'.format(number=i) + thumbnail_path = doc.get_thumbnail_folder() / 'slide{number:d}.png'.format(number=i) + while image_path.is_file(): + service_item.add_from_image(str(image_path), file_name, thumbnail=str(thumbnail_path)) i += 1 - image = str(doc.get_temp_folder() / 'mainslide{number:0>3d}.png'.format(number=i)) - thumbnail = str(doc.get_thumbnail_folder() / 'slide{number:d}.png'.format(number=i)) + image_path = doc.get_temp_folder() / 'mainslide{number:0>3d}.png'.format(number=i) + thumbnail_path = doc.get_thumbnail_folder() / 'slide{number:d}.png'.format(number=i) service_item.add_capability(ItemCapabilities.HasThumbnails) doc.close_presentation() return True @@ -324,34 +322,34 @@ class PresentationMediaItem(MediaManagerItem): critical_error_message_box(translate('PresentationPlugin.MediaItem', 'Missing Presentation'), translate('PresentationPlugin.MediaItem', 'The presentation {name} no longer exists.' - ).format(name=filename)) + ).format(name=file_path)) return False else: service_item.processor = self.display_type_combo_box.currentText() service_item.add_capability(ItemCapabilities.ProvidesOwnDisplay) for bitem in items: - filename = bitem.data(QtCore.Qt.UserRole) - (path, name) = os.path.split(filename) - service_item.title = name - if os.path.exists(filename): + file_path = str_to_path(bitem.data(QtCore.Qt.UserRole)) + path, file_name = file_path.parent, file_path.name + service_item.title = file_name + if file_path.exists: if self.display_type_combo_box.itemData(self.display_type_combo_box.currentIndex()) == 'automatic': - service_item.processor = self.find_controller_by_type(filename) + service_item.processor = self.find_controller_by_type(file_path) if not service_item.processor: return False controller = self.controllers[service_item.processor] - doc = controller.add_document(filename) + doc = controller.add_document(file_path) if doc.get_thumbnail_path(1, True) is None: doc.load_presentation() i = 1 - img = doc.get_thumbnail_path(i, True) - if img: + thumbnail_path = doc.get_thumbnail_path(i, True) + if thumbnail_path: # Get titles and notes titles, notes = doc.get_titles_and_notes() service_item.add_capability(ItemCapabilities.HasDisplayTitle) if notes.count('') != len(notes): service_item.add_capability(ItemCapabilities.HasNotes) service_item.add_capability(ItemCapabilities.HasThumbnails) - while img: + while thumbnail_path: # Use title and note if available title = '' if titles and len(titles) >= i: @@ -359,9 +357,9 @@ class PresentationMediaItem(MediaManagerItem): note = '' if notes and len(notes) >= i: note = notes[i - 1] - service_item.add_from_command(path, name, img, title, note) + service_item.add_from_command(str(path), file_name, str(thumbnail_path), title, note) i += 1 - img = doc.get_thumbnail_path(i, True) + thumbnail_path = doc.get_thumbnail_path(i, True) doc.close_presentation() return True else: @@ -371,7 +369,7 @@ class PresentationMediaItem(MediaManagerItem): 'Missing Presentation'), translate('PresentationPlugin.MediaItem', 'The presentation {name} is incomplete, ' - 'please reload.').format(name=filename)) + 'please reload.').format(name=file_path)) return False else: # File is no longer present @@ -379,18 +377,20 @@ class PresentationMediaItem(MediaManagerItem): critical_error_message_box(translate('PresentationPlugin.MediaItem', 'Missing Presentation'), translate('PresentationPlugin.MediaItem', 'The presentation {name} no longer exists.' - ).format(name=filename)) + ).format(name=file_path)) return False - def find_controller_by_type(self, filename): + def find_controller_by_type(self, file_path): """ Determine the default application controller to use for the selected file type. This is used if "Automatic" is set as the preferred controller. Find the first (alphabetic) enabled controller which "supports" the extension. If none found, then look for a controller which "also supports" it instead. - :param filename: The file name + :param openlp.core.common.path.Path file_path: The file path + :return: The default application controller for this file type, or None if not supported + :rtype: PresentationController """ - file_type = os.path.splitext(filename)[1][1:] + file_type = file_path.suffix[1:] if not file_type: return None for controller in self.controllers: diff --git a/openlp/plugins/presentations/lib/messagelistener.py b/openlp/plugins/presentations/lib/messagelistener.py index 8e5de3e2d..5ad46d0fe 100644 --- a/openlp/plugins/presentations/lib/messagelistener.py +++ b/openlp/plugins/presentations/lib/messagelistener.py @@ -19,16 +19,15 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### - -import logging import copy -import os +import logging from PyQt5 import QtCore from openlp.core.common import Registry, Settings -from openlp.core.ui import HideMode +from openlp.core.common.path import Path from openlp.core.lib import ServiceItemContext +from openlp.core.ui import HideMode from openlp.plugins.presentations.lib.pdfcontroller import PDF_CONTROLLER_FILETYPES log = logging.getLogger(__name__) @@ -325,21 +324,25 @@ class MessageListener(object): is_live = message[1] item = message[0] hide_mode = message[2] - file = item.get_frame_path() + file_path = Path(item.get_frame_path()) self.handler = item.processor # When starting presentation from the servicemanager we convert # PDF/XPS/OXPS-serviceitems into image-serviceitems. When started from the mediamanager # the conversion has already been done at this point. - file_type = os.path.splitext(file.lower())[1][1:] + file_type = file_path.suffix.lower()[1:] if file_type in PDF_CONTROLLER_FILETYPES: - log.debug('Converting from pdf/xps/oxps to images for serviceitem with file {name}'.format(name=file)) + log.debug('Converting from pdf/xps/oxps to images for serviceitem with file {name}'.format(name=file_path)) # Create a copy of the original item, and then clear the original item so it can be filled with images item_cpy = copy.copy(item) item.__init__(None) if is_live: - self.media_item.generate_slide_data(item, item_cpy, False, False, ServiceItemContext.Live, file) + # TODO: To Path object + self.media_item.generate_slide_data(item, item_cpy, False, False, ServiceItemContext.Live, + str(file_path)) else: - self.media_item.generate_slide_data(item, item_cpy, False, False, ServiceItemContext.Preview, file) + # TODO: To Path object + self.media_item.generate_slide_data(item, item_cpy, False, False, ServiceItemContext.Preview, + str(file_path)) # Some of the original serviceitem attributes is needed in the new serviceitem item.footer = item_cpy.footer item.from_service = item_cpy.from_service @@ -352,13 +355,13 @@ class MessageListener(object): self.handler = None else: if self.handler == self.media_item.automatic: - self.handler = self.media_item.find_controller_by_type(file) + self.handler = self.media_item.find_controller_by_type(file_path) if not self.handler: return else: - # the saved handler is not present so need to use one based on file suffix. + # the saved handler is not present so need to use one based on file_path suffix. if not self.controllers[self.handler].available: - self.handler = self.media_item.find_controller_by_type(file) + self.handler = self.media_item.find_controller_by_type(file_path) if not self.handler: return if is_live: @@ -370,7 +373,7 @@ class MessageListener(object): if self.handler is None: self.controller = controller else: - controller.add_handler(self.controllers[self.handler], file, hide_mode, message[3]) + controller.add_handler(self.controllers[self.handler], file_path, hide_mode, message[3]) self.timer.start() def slide(self, message): diff --git a/openlp/plugins/presentations/lib/pdfcontroller.py b/openlp/plugins/presentations/lib/pdfcontroller.py index f80ad94ba..9f4aa1b4f 100644 --- a/openlp/plugins/presentations/lib/pdfcontroller.py +++ b/openlp/plugins/presentations/lib/pdfcontroller.py @@ -23,13 +23,13 @@ import os import logging import re -from shutil import which from subprocess import check_output, CalledProcessError from openlp.core.common import AppLocation, check_binary_exists from openlp.core.common import Settings, is_win from openlp.core.common.path import Path, path_to_str from openlp.core.lib import ScreenList +from openlp.core.lib.shutil import which from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument if is_win(): @@ -66,11 +66,12 @@ class PdfController(PresentationController): Function that checks whether a binary is either ghostscript or mudraw or neither. Is also used from presentationtab.py - :param program_path:The full path to the binary to check. + :param openlp.core.common.path.Path program_path: The full path to the binary to check. :return: Type of the binary, 'gs' if ghostscript, 'mudraw' if mudraw, None if invalid. + :rtype: str | None """ program_type = None - runlog = check_binary_exists(Path(program_path)) + runlog = check_binary_exists(program_path) # Analyse the output to see it the program is mudraw, ghostscript or neither for line in runlog.splitlines(): decoded_line = line.decode() @@ -107,30 +108,29 @@ class PdfController(PresentationController): :return: True if program to open PDF-files was found, otherwise False. """ log.debug('check_installed Pdf') - self.mudrawbin = '' - self.mutoolbin = '' - self.gsbin = '' + self.mudrawbin = None + self.mutoolbin = None + self.gsbin = None self.also_supports = [] # Use the user defined program if given if Settings().value('presentations/enable_pdf_program'): - pdf_program = path_to_str(Settings().value('presentations/pdf_program')) - program_type = self.process_check_binary(pdf_program) + program_path = Settings().value('presentations/pdf_program') + program_type = self.process_check_binary(program_path) if program_type == 'gs': - self.gsbin = pdf_program + self.gsbin = program_path elif program_type == 'mudraw': - self.mudrawbin = pdf_program + self.mudrawbin = program_path elif program_type == 'mutool': - self.mutoolbin = pdf_program + self.mutoolbin = program_path else: # Fallback to autodetection - application_path = str(AppLocation.get_directory(AppLocation.AppDir)) + application_path = AppLocation.get_directory(AppLocation.AppDir) if is_win(): # for windows we only accept mudraw.exe or mutool.exe in the base folder - application_path = str(AppLocation.get_directory(AppLocation.AppDir)) - if os.path.isfile(os.path.join(application_path, 'mudraw.exe')): - self.mudrawbin = os.path.join(application_path, 'mudraw.exe') - elif os.path.isfile(os.path.join(application_path, 'mutool.exe')): - self.mutoolbin = os.path.join(application_path, 'mutool.exe') + if (application_path / 'mudraw.exe').is_file(): + self.mudrawbin = application_path / 'mudraw.exe' + elif (application_path / 'mutool.exe').is_file(): + self.mutoolbin = application_path / 'mutool.exe' else: DEVNULL = open(os.devnull, 'wb') # First try to find mudraw @@ -143,11 +143,11 @@ class PdfController(PresentationController): self.gsbin = which('gs') # Last option: check if mudraw or mutool is placed in OpenLP base folder if not self.mudrawbin and not self.mutoolbin and not self.gsbin: - application_path = str(AppLocation.get_directory(AppLocation.AppDir)) - if os.path.isfile(os.path.join(application_path, 'mudraw')): - self.mudrawbin = os.path.join(application_path, 'mudraw') - elif os.path.isfile(os.path.join(application_path, 'mutool')): - self.mutoolbin = os.path.join(application_path, 'mutool') + application_path = AppLocation.get_directory(AppLocation.AppDir) + if (application_path / 'mudraw').is_file(): + self.mudrawbin = application_path / 'mudraw' + elif (application_path / 'mutool').is_file(): + self.mutoolbin = application_path / 'mutool' if self.mudrawbin or self.mutoolbin: self.also_supports = ['xps', 'oxps'] return True @@ -172,12 +172,15 @@ class PdfDocument(PresentationDocument): image-serviceitem on the fly and present as such. Therefore some of the 'playback' functions is not implemented. """ - def __init__(self, controller, presentation): + def __init__(self, controller, document_path): """ Constructor, store information about the file and initialise. + + :param openlp.core.common.path.Path document_path: Path to the document to load + :rtype: None """ log.debug('Init Presentation Pdf') - PresentationDocument.__init__(self, controller, presentation) + super().__init__(controller, document_path) self.presentation = None self.blanked = False self.hidden = False @@ -200,13 +203,13 @@ class PdfDocument(PresentationDocument): :return: The resolution dpi to be used. """ # Use a postscript script to get size of the pdf. It is assumed that all pages have same size - gs_resolution_script = str(AppLocation.get_directory( - AppLocation.PluginsDir)) + '/presentations/lib/ghostscript_get_resolution.ps' + gs_resolution_script = AppLocation.get_directory( + AppLocation.PluginsDir) / 'presentations' / 'lib' / 'ghostscript_get_resolution.ps' # Run the script on the pdf to get the size runlog = [] try: - runlog = check_output([self.controller.gsbin, '-dNOPAUSE', '-dNODISPLAY', '-dBATCH', - '-sFile=' + self.file_path, gs_resolution_script], + runlog = check_output([str(self.controller.gsbin), '-dNOPAUSE', '-dNODISPLAY', '-dBATCH', + '-sFile={file_path}'.format(file_path=self.file_path), str(gs_resolution_script)], startupinfo=self.startupinfo) except CalledProcessError as e: log.debug(' '.join(e.cmd)) @@ -246,7 +249,7 @@ class PdfDocument(PresentationDocument): created_files = sorted(temp_dir_path.glob('*')) for image_path in created_files: if image_path.is_file(): - self.image_files.append(str(image_path)) + self.image_files.append(image_path) self.num_pages = len(self.image_files) return True size = ScreenList().current['size'] @@ -258,27 +261,27 @@ class PdfDocument(PresentationDocument): # The %03d in the file name is handled by each binary if self.controller.mudrawbin: log.debug('loading presentation using mudraw') - runlog = check_output([self.controller.mudrawbin, '-w', str(size.width()), '-h', str(size.height()), - '-o', str(temp_dir_path / 'mainslide%03d.png'), self.file_path], + runlog = check_output([str(self.controller.mudrawbin), '-w', str(size.width()), '-h', str(size.height()), + '-o', str(temp_dir_path / 'mainslide%03d.png'), str(self.file_path)], startupinfo=self.startupinfo) elif self.controller.mutoolbin: log.debug('loading presentation using mutool') - runlog = check_output([self.controller.mutoolbin, 'draw', '-w', str(size.width()), '-h', - str(size.height()), - '-o', str(temp_dir_path / 'mainslide%03d.png'), self.file_path], + runlog = check_output([str(self.controller.mutoolbin), 'draw', '-w', str(size.width()), + '-h', str(size.height()), '-o', str(temp_dir_path / 'mainslide%03d.png'), + str(self.file_path)], startupinfo=self.startupinfo) elif self.controller.gsbin: log.debug('loading presentation using gs') resolution = self.gs_get_resolution(size) - runlog = check_output([self.controller.gsbin, '-dSAFER', '-dNOPAUSE', '-dBATCH', '-sDEVICE=png16m', - '-r' + str(resolution), '-dTextAlphaBits=4', '-dGraphicsAlphaBits=4', - '-sOutputFile=' + str(temp_dir_path / 'mainslide%03d.png'), - self.file_path], startupinfo=self.startupinfo) + runlog = check_output([str(self.controller.gsbin), '-dSAFER', '-dNOPAUSE', '-dBATCH', '-sDEVICE=png16m', + '-r{res}'.format(res=resolution), '-dTextAlphaBits=4', '-dGraphicsAlphaBits=4', + '-sOutputFile={output}'.format(output=temp_dir_path / 'mainslide%03d.png'), + str(self.file_path)], startupinfo=self.startupinfo) created_files = sorted(temp_dir_path.glob('*')) for image_path in created_files: if image_path.is_file(): - self.image_files.append(str(image_path)) - except Exception: + self.image_files.append(image_path) + except Exception as e: log.exception(runlog) return False self.num_pages = len(self.image_files) diff --git a/openlp/plugins/presentations/lib/powerpointcontroller.py b/openlp/plugins/presentations/lib/powerpointcontroller.py index 1014db851..fa253ffda 100644 --- a/openlp/plugins/presentations/lib/powerpointcontroller.py +++ b/openlp/plugins/presentations/lib/powerpointcontroller.py @@ -120,15 +120,16 @@ class PowerpointDocument(PresentationDocument): Class which holds information and controls a single presentation. """ - def __init__(self, controller, presentation): + def __init__(self, controller, document_path): """ Constructor, store information about the file and initialise. :param controller: - :param presentation: + :param openlp.core.common.path.Path document_path: Path to the document to load + :rtype: None """ log.debug('Init Presentation Powerpoint') - super(PowerpointDocument, self).__init__(controller, presentation) + super().__init__(controller, document_path) self.presentation = None self.index_map = {} self.slide_count = 0 @@ -145,7 +146,7 @@ class PowerpointDocument(PresentationDocument): try: if not self.controller.process: self.controller.start_process() - self.controller.process.Presentations.Open(os.path.normpath(self.file_path), False, False, False) + self.controller.process.Presentations.Open(str(self.file_path), False, False, False) self.presentation = self.controller.process.Presentations(self.controller.process.Presentations.Count) self.create_thumbnails() self.create_titles_and_notes() @@ -363,9 +364,8 @@ class PowerpointDocument(PresentationDocument): width=size.width(), horizontal=(right - left))) log.debug('window title: {title}'.format(title=window_title)) - filename_root, filename_ext = os.path.splitext(os.path.basename(self.file_path)) if size.y() == top and size.height() == (bottom - top) and size.x() == left and \ - size.width() == (right - left) and filename_root in window_title: + size.width() == (right - left) and self.file_path.stem in window_title: log.debug('Found a match and will save the handle') self.presentation_hwnd = hwnd # Stop powerpoint from flashing in the taskbar diff --git a/openlp/plugins/presentations/lib/pptviewcontroller.py b/openlp/plugins/presentations/lib/pptviewcontroller.py index c936fe65c..547636026 100644 --- a/openlp/plugins/presentations/lib/pptviewcontroller.py +++ b/openlp/plugins/presentations/lib/pptviewcontroller.py @@ -85,9 +85,9 @@ class PptviewController(PresentationController): if self.process: return log.debug('start PPTView') - dll_path = os.path.join(str(AppLocation.get_directory(AppLocation.AppDir)), - 'plugins', 'presentations', 'lib', 'pptviewlib', 'pptviewlib.dll') - self.process = cdll.LoadLibrary(dll_path) + dll_path = AppLocation.get_directory(AppLocation.AppDir) \ + / 'plugins' / 'presentations' / 'lib' / 'pptviewlib' / 'pptviewlib.dll' + self.process = cdll.LoadLibrary(str(dll_path)) if log.isEnabledFor(logging.DEBUG): self.process.SetDebug(1) @@ -104,12 +104,15 @@ class PptviewDocument(PresentationDocument): """ Class which holds information and controls a single presentation. """ - def __init__(self, controller, presentation): + def __init__(self, controller, document_path): """ Constructor, store information about the file and initialise. + + :param openlp.core.common.path.Path document_path: File path to the document to load + :rtype: None """ log.debug('Init Presentation PowerPoint') - super(PptviewDocument, self).__init__(controller, presentation) + super().__init__(controller, document_path) self.presentation = None self.ppt_id = None self.blanked = False @@ -121,17 +124,16 @@ class PptviewDocument(PresentationDocument): the background PptView task started earlier. """ log.debug('LoadPresentation') - temp_dir_path = self.get_temp_folder() + temp_path = self.get_temp_folder() size = ScreenList().current['size'] rect = RECT(size.x(), size.y(), size.right(), size.bottom()) - self.file_path = os.path.normpath(self.file_path) - preview_path = temp_dir_path / 'slide' + preview_path = temp_path / 'slide' # Ensure that the paths are null terminated - byte_file_path = self.file_path.encode('utf-16-le') + b'\0' - preview_file_name = str(preview_path).encode('utf-16-le') + b'\0' - if not temp_dir_path: - temp_dir_path.mkdir(parents=True) - self.ppt_id = self.controller.process.OpenPPT(byte_file_path, None, rect, preview_file_name) + file_path_utf16 = str(self.file_path).encode('utf-16-le') + b'\0' + preview_path_utf16 = str(preview_path).encode('utf-16-le') + b'\0' + if not temp_path.is_dir(): + temp_path.mkdir(parents=True) + self.ppt_id = self.controller.process.OpenPPT(file_path_utf16, None, rect, preview_path_utf16) if self.ppt_id >= 0: self.create_thumbnails() self.stop_presentation() @@ -148,7 +150,7 @@ class PptviewDocument(PresentationDocument): return log.debug('create_thumbnails proceeding') for idx in range(self.get_slide_count()): - path = '{folder}\\slide{index}.bmp'.format(folder=self.get_temp_folder(), index=str(idx + 1)) + path = self.get_temp_folder() / 'slide{index:d}.bmp'.format(index=idx + 1) self.convert_thumbnail(path, idx + 1) def create_titles_and_notes(self): @@ -161,13 +163,12 @@ class PptviewDocument(PresentationDocument): """ titles = None notes = None - filename = os.path.normpath(self.file_path) # let's make sure we have a valid zipped presentation - if os.path.exists(filename) and zipfile.is_zipfile(filename): + if self.file_path.exists() and zipfile.is_zipfile(str(self.file_path)): namespaces = {"p": "http://schemas.openxmlformats.org/presentationml/2006/main", "a": "http://schemas.openxmlformats.org/drawingml/2006/main"} # open the file - with zipfile.ZipFile(filename) as zip_file: + with zipfile.ZipFile(str(self.file_path)) as zip_file: # find the presentation.xml to get the slide count with zip_file.open('ppt/presentation.xml') as pres: tree = ElementTree.parse(pres) diff --git a/openlp/plugins/presentations/lib/presentationcontroller.py b/openlp/plugins/presentations/lib/presentationcontroller.py index af665bb55..13a759a5e 100644 --- a/openlp/plugins/presentations/lib/presentationcontroller.py +++ b/openlp/plugins/presentations/lib/presentationcontroller.py @@ -19,10 +19,7 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### - import logging -import os -import shutil from PyQt5 import QtCore @@ -87,19 +84,26 @@ class PresentationDocument(object): Returns a path to an image containing a preview for the requested slide """ - def __init__(self, controller, name): + def __init__(self, controller, document_path): """ Constructor for the PresentationController class + + :param controller: + :param openlp.core.common.path.Path document_path: Path to the document to load. + :rtype: None """ self.controller = controller - self._setup(name) + self._setup(document_path) - def _setup(self, name): + def _setup(self, document_path): """ Run some initial setup. This method is separate from __init__ in order to mock it out in tests. + + :param openlp.core.common.path.Path document_path: Path to the document to load. + :rtype: None """ self.slide_number = 0 - self.file_path = name + self.file_path = document_path check_directory_exists(self.get_thumbnail_folder()) def load_presentation(self): @@ -126,12 +130,6 @@ class PresentationDocument(object): except OSError: log.exception('Failed to delete presentation controller files') - def get_file_name(self): - """ - Return just the filename of the presentation, without the directory - """ - return os.path.split(self.file_path)[1] - def get_thumbnail_folder(self): """ The location where thumbnail images will be stored @@ -141,9 +139,9 @@ class PresentationDocument(object): """ # TODO: If statement can be removed when the upgrade path from 2.0.x to 2.2.x is no longer needed if Settings().value('presentations/thumbnail_scheme') == 'md5': - folder = md5_hash(self.file_path.encode('utf-8')) + folder = md5_hash(bytes(self.file_path)) else: - folder = self.get_file_name() + folder = self.file_path.name return Path(self.controller.thumbnail_folder, folder) def get_temp_folder(self): @@ -155,19 +153,22 @@ class PresentationDocument(object): """ # TODO: If statement can be removed when the upgrade path from 2.0.x to 2.2.x is no longer needed if Settings().value('presentations/thumbnail_scheme') == 'md5': - folder = md5_hash(self.file_path.encode('utf-8')) + folder = md5_hash(bytes(self.file_path)) else: - folder = self.get_file_name() + folder = self.file_path.name return Path(self.controller.temp_folder, folder) def check_thumbnails(self): """ - Returns ``True`` if the thumbnail images exist and are more recent than the powerpoint file. + Check that the last thumbnail image exists and is valid and are more recent than the powerpoint file. + + :return: If the thumbnail is valid + :rtype: bool """ - last_image = self.get_thumbnail_path(self.get_slide_count(), True) - if not (last_image and os.path.isfile(last_image)): + last_image_path = self.get_thumbnail_path(self.get_slide_count(), True) + if not (last_image_path and last_image_path.is_file()): return False - return validate_thumb(self.file_path, last_image) + return validate_thumb(self.file_path, last_image_path) def close_presentation(self): """ @@ -250,24 +251,28 @@ class PresentationDocument(object): """ pass - def convert_thumbnail(self, file, idx): + def convert_thumbnail(self, image_path, index): """ Convert the slide image the application made to a scaled 360px height .png image. + + :param openlp.core.common.path.Path image_path: Path to the image to create a thumb nail of + :param int index: The index of the slide to create the thumbnail for. + :rtype: None """ if self.check_thumbnails(): return - if os.path.isfile(file): - thumb_path = self.get_thumbnail_path(idx, False) - create_thumb(file, thumb_path, False, QtCore.QSize(-1, 360)) + if image_path.is_file(): + thumb_path = self.get_thumbnail_path(index, False) + create_thumb(str(image_path), str(thumb_path), False, QtCore.QSize(-1, 360)) - def get_thumbnail_path(self, slide_no, check_exists=True): + def get_thumbnail_path(self, slide_no, check_exists=False): """ Returns an image path containing a preview for the requested slide :param int slide_no: The slide an image is required for, starting at 1 :param bool check_exists: Check if the generated path exists :return: The path, or None if the :param:`check_exists` is True and the file does not exist - :rtype: openlp.core.common.path.Path, None + :rtype: openlp.core.common.path.Path | None """ path = self.get_thumbnail_folder() / (self.controller.thumbnail_prefix + str(slide_no) + '.png') if path.is_file() or not check_exists: @@ -313,43 +318,38 @@ class PresentationDocument(object): Reads the titles from the titles file and the notes files and returns the content in two lists """ - titles = [] notes = [] - titles_file = str(self.get_thumbnail_folder() / 'titles.txt') - if os.path.exists(titles_file): - try: - with open(titles_file, encoding='utf-8') as fi: - titles = fi.read().splitlines() - except: - log.exception('Failed to open/read existing titles file') - titles = [] + titles_path = self.get_thumbnail_folder() / 'titles.txt' + try: + titles = titles_path.read_text().splitlines() + except: + log.exception('Failed to open/read existing titles file') + titles = [] for slide_no, title in enumerate(titles, 1): - notes_file = str(self.get_thumbnail_folder() / 'slideNotes{number:d}.txt'.format(number=slide_no)) - note = '' - if os.path.exists(notes_file): - try: - with open(notes_file, encoding='utf-8') as fn: - note = fn.read() - except: - log.exception('Failed to open/read notes file') - note = '' + notes_path = self.get_thumbnail_folder() / 'slideNotes{number:d}.txt'.format(number=slide_no) + try: + note = notes_path.read_text() + except: + log.exception('Failed to open/read notes file') + note = '' notes.append(note) return titles, notes def save_titles_and_notes(self, titles, notes): """ - Performs the actual persisting of titles to the titles.txt - and notes to the slideNote%.txt + Performs the actual persisting of titles to the titles.txt and notes to the slideNote%.txt + + :param list[str] titles: The titles to save + :param list[str] notes: The notes to save + :rtype: None """ if titles: titles_path = self.get_thumbnail_folder() / 'titles.txt' - with titles_path.open(mode='wt', encoding='utf-8') as fo: - fo.writelines(titles) + titles_path.write_text('\n'.join(titles)) if notes: for slide_no, note in enumerate(notes, 1): notes_path = self.get_thumbnail_folder() / 'slideNotes{number:d}.txt'.format(number=slide_no) - with notes_path.open(mode='wt', encoding='utf-8') as fn: - fn.write(note) + notes_path.write_text(note) class PresentationController(object): @@ -426,12 +426,11 @@ class PresentationController(object): self.document_class = document_class self.settings_section = self.plugin.settings_section self.available = None - self.temp_folder = os.path.join(str(AppLocation.get_section_data_path(self.settings_section)), name) - self.thumbnail_folder = os.path.join( - str(AppLocation.get_section_data_path(self.settings_section)), 'thumbnails') + self.temp_folder = AppLocation.get_section_data_path(self.settings_section) / name + self.thumbnail_folder = AppLocation.get_section_data_path(self.settings_section) / 'thumbnails' self.thumbnail_prefix = 'slide' - check_directory_exists(Path(self.thumbnail_folder)) - check_directory_exists(Path(self.temp_folder)) + check_directory_exists(self.thumbnail_folder) + check_directory_exists(self.temp_folder) def enabled(self): """ @@ -466,11 +465,15 @@ class PresentationController(object): log.debug('Kill') self.close_presentation() - def add_document(self, name): + def add_document(self, document_path): """ Called when a new presentation document is opened. + + :param openlp.core.common.path.Path document_path: Path to the document to load + :return: The document + :rtype: PresentationDocument """ - document = self.document_class(self, name) + document = self.document_class(self, document_path) self.docs.append(document) return document diff --git a/openlp/plugins/presentations/lib/presentationtab.py b/openlp/plugins/presentations/lib/presentationtab.py index 3e92827c8..ca9ceacbc 100644 --- a/openlp/plugins/presentations/lib/presentationtab.py +++ b/openlp/plugins/presentations/lib/presentationtab.py @@ -38,7 +38,6 @@ class PresentationTab(SettingsTab): """ Constructor """ - self.parent = parent self.controllers = controllers super(PresentationTab, self).__init__(parent, title, visible_title, icon_path) self.activated = False @@ -194,7 +193,7 @@ class PresentationTab(SettingsTab): pdf_program_path = self.program_path_edit.path enable_pdf_program = self.pdf_program_check_box.checkState() # If the given program is blank disable using the program - if not pdf_program_path: + if pdf_program_path is None: enable_pdf_program = 0 if pdf_program_path != Settings().value(self.settings_section + '/pdf_program'): Settings().setValue(self.settings_section + '/pdf_program', pdf_program_path) @@ -220,9 +219,11 @@ class PresentationTab(SettingsTab): def on_program_path_edit_path_changed(self, new_path): """ - Select the mudraw or ghostscript binary that should be used. + Handle the `pathEditChanged` signal from program_path_edit + + :param openlp.core.common.path.Path new_path: File path to the new program + :rtype: None """ - new_path = path_to_str(new_path) if new_path: if not PdfController.process_check_binary(new_path): critical_error_message_box(UiStrings().Error, diff --git a/tests/functional/openlp_plugins/presentations/test_presentationcontroller.py b/tests/functional/openlp_plugins/presentations/test_presentationcontroller.py index 1bbd29522..c5e6d3df3 100644 --- a/tests/functional/openlp_plugins/presentations/test_presentationcontroller.py +++ b/tests/functional/openlp_plugins/presentations/test_presentationcontroller.py @@ -244,20 +244,3 @@ class TestPresentationDocument(TestCase): # THEN: load_presentation should return false self.assertFalse(result, "PresentationDocument.load_presentation should return false.") - - def test_get_file_name(self): - """ - Test the PresentationDocument.get_file_name method. - """ - - # GIVEN: A mocked os.path.split which returns a list, an instance of PresentationDocument and - # arbitary file_path. - self.mock_os.path.split.return_value = ['directory', 'file.ext'] - instance = PresentationDocument(self.mock_controller, 'Name') - instance.file_path = 'filepath' - - # WHEN: Calling get_file_name - result = instance.get_file_name() - - # THEN: get_file_name should return 'file.ext' - self.assertEqual(result, 'file.ext') From 7f98003d5492603c62db5fac1d1c000ebef4d442 Mon Sep 17 00:00:00 2001 From: Philip Ridout Date: Sun, 17 Sep 2017 20:43:15 +0100 Subject: [PATCH 06/13] test fixes --- openlp/core/lib/__init__.py | 11 ++-- openlp/core/ui/thememanager.py | 2 +- openlp/plugins/images/lib/mediaitem.py | 2 +- openlp/plugins/presentations/lib/mediaitem.py | 2 +- .../presentations/lib/pdfcontroller.py | 3 +- .../lib/presentationcontroller.py | 2 +- tests/functional/openlp_core_lib/test_lib.py | 55 ++++++---------- .../presentations/test_impresscontroller.py | 7 +- .../presentations/test_mediaitem.py | 19 +++--- .../presentations/test_pdfcontroller.py | 15 +++-- .../presentations/test_pptviewcontroller.py | 14 ++-- .../test_presentationcontroller.py | 65 +++++++------------ 12 files changed, 83 insertions(+), 114 deletions(-) diff --git a/openlp/core/lib/__init__.py b/openlp/core/lib/__init__.py index 0babbc0d1..1d55df497 100644 --- a/openlp/core/lib/__init__.py +++ b/openlp/core/lib/__init__.py @@ -29,7 +29,7 @@ import os import re import math -from PyQt5 import QtCore, QtGui, QtWidgets +from PyQt5 import QtCore, QtGui, Qt, QtWidgets from openlp.core.common import translate from openlp.core.common.path import Path @@ -221,12 +221,11 @@ def validate_thumb(file_path, thumb_path): Validates whether an file's thumb still exists and if is up to date. **Note**, you must **not** call this function, before checking the existence of the file. - :param file_path: The path to the file. The file **must** exist! - :param thumb_path: The path to the thumb. - :return: True, False if the image has changed since the thumb was created. + :param openlp.core.common.path.Path file_path: The path to the file. The file **must** exist! + :param openlp.core.common.path.Path thumb_path: The path to the thumb. + :return: Has the image changed since the thumb was created? + :rtype: bool """ - file_path = Path(file_path) - thumb_path = Path(thumb_path) if not thumb_path.exists(): return False image_date = file_path.stat().st_mtime diff --git a/openlp/core/ui/thememanager.py b/openlp/core/ui/thememanager.py index 61381a902..15e33cdb2 100644 --- a/openlp/core/ui/thememanager.py +++ b/openlp/core/ui/thememanager.py @@ -483,7 +483,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage name = text_name thumb = os.path.join(self.thumb_path, '{name}.png'.format(name=text_name)) item_name = QtWidgets.QListWidgetItem(name) - if validate_thumb(theme, thumb): + if validate_thumb(Path(theme), Path(thumb)): icon = build_icon(thumb) else: icon = create_thumb(theme, thumb) diff --git a/openlp/plugins/images/lib/mediaitem.py b/openlp/plugins/images/lib/mediaitem.py index bcf222eb0..d1ea2003f 100644 --- a/openlp/plugins/images/lib/mediaitem.py +++ b/openlp/plugins/images/lib/mediaitem.py @@ -360,7 +360,7 @@ class ImageMediaItem(MediaManagerItem): if not os.path.exists(image_file.filename): icon = build_icon(':/general/general_delete.png') else: - if validate_thumb(image_file.filename, thumb): + if validate_thumb(Path(image_file.filename), Path(thumb)): icon = build_icon(thumb) else: icon = create_thumb(image_file.filename, thumb) diff --git a/openlp/plugins/presentations/lib/mediaitem.py b/openlp/plugins/presentations/lib/mediaitem.py index aa5bfc0d6..d9a14e0ed 100644 --- a/openlp/plugins/presentations/lib/mediaitem.py +++ b/openlp/plugins/presentations/lib/mediaitem.py @@ -197,7 +197,7 @@ class PresentationMediaItem(MediaManagerItem): if not (preview_path and preview_path.exists()): icon = build_icon(':/general/general_delete.png') else: - if validate_thumb(preview_path, thumbnail_path): + if validate_thumb(Path(preview_path), Path(thumbnail_path)): icon = build_icon(thumbnail_path) else: icon = create_thumb(str(preview_path), str(thumbnail_path)) diff --git a/openlp/plugins/presentations/lib/pdfcontroller.py b/openlp/plugins/presentations/lib/pdfcontroller.py index 9f4aa1b4f..a39cce36c 100644 --- a/openlp/plugins/presentations/lib/pdfcontroller.py +++ b/openlp/plugins/presentations/lib/pdfcontroller.py @@ -261,7 +261,8 @@ class PdfDocument(PresentationDocument): # The %03d in the file name is handled by each binary if self.controller.mudrawbin: log.debug('loading presentation using mudraw') - runlog = check_output([str(self.controller.mudrawbin), '-w', str(size.width()), '-h', str(size.height()), + runlog = check_output([str(self.controller.mudrawbin), '-w', str(size.width()), + '-h', str(size.height()), '-o', str(temp_dir_path / 'mainslide%03d.png'), str(self.file_path)], startupinfo=self.startupinfo) elif self.controller.mutoolbin: diff --git a/openlp/plugins/presentations/lib/presentationcontroller.py b/openlp/plugins/presentations/lib/presentationcontroller.py index 13a759a5e..3225eac24 100644 --- a/openlp/plugins/presentations/lib/presentationcontroller.py +++ b/openlp/plugins/presentations/lib/presentationcontroller.py @@ -168,7 +168,7 @@ class PresentationDocument(object): last_image_path = self.get_thumbnail_path(self.get_slide_count(), True) if not (last_image_path and last_image_path.is_file()): return False - return validate_thumb(self.file_path, last_image_path) + return validate_thumb(Path(self.file_path), Path(last_image_path)) def close_presentation(self): """ diff --git a/tests/functional/openlp_core_lib/test_lib.py b/tests/functional/openlp_core_lib/test_lib.py index 2056665f4..8b46e99c3 100644 --- a/tests/functional/openlp_core_lib/test_lib.py +++ b/tests/functional/openlp_core_lib/test_lib.py @@ -595,61 +595,46 @@ class TestLib(TestCase): Test the validate_thumb() function when the thumbnail does not exist """ # GIVEN: A mocked out os module, with path.exists returning False, and fake paths to a file and a thumb - with patch('openlp.core.lib.os') as mocked_os: - file_path = 'path/to/file' - thumb_path = 'path/to/thumb' - mocked_os.path.exists.return_value = False + with patch.object(Path, 'exists', return_value=False) as mocked_path_exists: + file_path = Path('path', 'to', 'file') + thumb_path = Path('path', 'to', 'thumb') # WHEN: we run the validate_thumb() function result = validate_thumb(file_path, thumb_path) # THEN: we should have called a few functions, and the result should be False - mocked_os.path.exists.assert_called_with(thumb_path) - assert result is False, 'The result should be False' + thumb_path.exists.assert_called_once_with() + self.assertFalse(result, 'The result should be False') def test_validate_thumb_file_exists_and_newer(self): """ Test the validate_thumb() function when the thumbnail exists and has a newer timestamp than the file """ - # GIVEN: A mocked out os module, functions rigged to work for us, and fake paths to a file and a thumb - with patch('openlp.core.lib.os') as mocked_os: - file_path = 'path/to/file' - thumb_path = 'path/to/thumb' - file_mocked_stat = MagicMock() - file_mocked_stat.st_mtime = datetime.now() - thumb_mocked_stat = MagicMock() - thumb_mocked_stat.st_mtime = datetime.now() + timedelta(seconds=10) - mocked_os.path.exists.return_value = True - mocked_os.stat.side_effect = [file_mocked_stat, thumb_mocked_stat] + with patch.object(Path, 'exists'), patch.object(Path, 'stat'): + # GIVEN: Mocked file_path and thumb_path which return different values fo the modified times + file_path = MagicMock(**{'stat.return_value': MagicMock(st_mtime=10)}) + thumb_path = MagicMock(**{'exists.return_value': True, 'stat.return_value': MagicMock(st_mtime=11)}) # WHEN: we run the validate_thumb() function + result = validate_thumb(file_path, thumb_path) - # THEN: we should have called a few functions, and the result should be True - # mocked_os.path.exists.assert_called_with(thumb_path) + # THEN: `validate_thumb` should return True + self.assertTrue(result) def test_validate_thumb_file_exists_and_older(self): """ Test the validate_thumb() function when the thumbnail exists but is older than the file """ - # GIVEN: A mocked out os module, functions rigged to work for us, and fake paths to a file and a thumb - with patch('openlp.core.lib.os') as mocked_os: - file_path = 'path/to/file' - thumb_path = 'path/to/thumb' - file_mocked_stat = MagicMock() - file_mocked_stat.st_mtime = datetime.now() - thumb_mocked_stat = MagicMock() - thumb_mocked_stat.st_mtime = datetime.now() - timedelta(seconds=10) - mocked_os.path.exists.return_value = True - mocked_os.stat.side_effect = lambda fname: file_mocked_stat if fname == file_path else thumb_mocked_stat + # GIVEN: Mocked file_path and thumb_path which return different values fo the modified times + file_path = MagicMock(**{'stat.return_value': MagicMock(st_mtime=10)}) + thumb_path = MagicMock(**{'exists.return_value': True, 'stat.return_value': MagicMock(st_mtime=9)}) - # WHEN: we run the validate_thumb() function - result = validate_thumb(file_path, thumb_path) + # WHEN: we run the validate_thumb() function + result = validate_thumb(file_path, thumb_path) - # THEN: we should have called a few functions, and the result should be False - mocked_os.path.exists.assert_called_with(thumb_path) - mocked_os.stat.assert_any_call(file_path) - mocked_os.stat.assert_any_call(thumb_path) - assert result is False, 'The result should be False' + # THEN: `validate_thumb` should return False + thumb_path.stat.assert_called_once_with() + self.assertFalse(result, 'The result should be False') def test_replace_params_no_params(self): """ diff --git a/tests/functional/openlp_plugins/presentations/test_impresscontroller.py b/tests/functional/openlp_plugins/presentations/test_impresscontroller.py index d383b16e4..a792988e2 100644 --- a/tests/functional/openlp_plugins/presentations/test_impresscontroller.py +++ b/tests/functional/openlp_plugins/presentations/test_impresscontroller.py @@ -24,13 +24,12 @@ Functional tests to test the Impress class and related methods. """ from unittest import TestCase from unittest.mock import MagicMock -import os import shutil from tempfile import mkdtemp from openlp.core.common import Settings -from openlp.plugins.presentations.lib.impresscontroller import \ - ImpressController, ImpressDocument, TextType +from openlp.core.common.path import Path +from openlp.plugins.presentations.lib.impresscontroller import ImpressController, ImpressDocument, TextType from openlp.plugins.presentations.presentationplugin import __default_settings__ from tests.utils.constants import TEST_RESOURCES_PATH @@ -82,7 +81,7 @@ class TestImpressDocument(TestCase): mocked_plugin = MagicMock() mocked_plugin.settings_section = 'presentations' Settings().extend_default_settings(__default_settings__) - self.file_name = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'test.pptx') + self.file_name = Path(TEST_RESOURCES_PATH, 'presentations', 'test.pptx') self.ppc = ImpressController(mocked_plugin) self.doc = ImpressDocument(self.ppc, self.file_name) diff --git a/tests/functional/openlp_plugins/presentations/test_mediaitem.py b/tests/functional/openlp_plugins/presentations/test_mediaitem.py index b5299d785..9ce0a5fdc 100644 --- a/tests/functional/openlp_plugins/presentations/test_mediaitem.py +++ b/tests/functional/openlp_plugins/presentations/test_mediaitem.py @@ -26,6 +26,7 @@ from unittest import TestCase from unittest.mock import patch, MagicMock, call from openlp.core.common import Registry +from openlp.core.common.path import Path from openlp.plugins.presentations.lib.mediaitem import PresentationMediaItem from tests.helpers.testmixin import TestMixin @@ -92,17 +93,18 @@ class TestMediaItem(TestCase, TestMixin): """ # GIVEN: A mocked controller, and mocked os.path.getmtime mocked_controller = MagicMock() - mocked_doc = MagicMock() + mocked_doc = MagicMock(**{'get_thumbnail_path.return_value': Path()}) mocked_controller.add_document.return_value = mocked_doc mocked_controller.supports = ['tmp'] self.media_item.controllers = { 'Mocked': mocked_controller } - presentation_file = 'file.tmp' - with patch('openlp.plugins.presentations.lib.mediaitem.os.path.getmtime') as mocked_getmtime, \ - patch('openlp.plugins.presentations.lib.mediaitem.os.path.exists') as mocked_exists: - mocked_getmtime.side_effect = [100, 200] - mocked_exists.return_value = True + + thmub_path = MagicMock(st_mtime=100) + file_path = MagicMock(st_mtime=400) + with patch.object(Path, 'stat', side_effect=[thmub_path, file_path]), \ + patch.object(Path, 'exists', return_value=True): + presentation_file = Path('file.tmp') # WHEN: calling clean_up_thumbnails self.media_item.clean_up_thumbnails(presentation_file, True) @@ -123,9 +125,8 @@ class TestMediaItem(TestCase, TestMixin): self.media_item.controllers = { 'Mocked': mocked_controller } - presentation_file = 'file.tmp' - with patch('openlp.plugins.presentations.lib.mediaitem.os.path.exists') as mocked_exists: - mocked_exists.return_value = False + presentation_file = Path('file.tmp') + with patch.object(Path, 'exists', return_value=False): # WHEN: calling clean_up_thumbnails self.media_item.clean_up_thumbnails(presentation_file, True) diff --git a/tests/functional/openlp_plugins/presentations/test_pdfcontroller.py b/tests/functional/openlp_plugins/presentations/test_pdfcontroller.py index be4aeeaa4..25a8394f0 100644 --- a/tests/functional/openlp_plugins/presentations/test_pdfcontroller.py +++ b/tests/functional/openlp_plugins/presentations/test_pdfcontroller.py @@ -32,6 +32,7 @@ from PyQt5 import QtCore, QtGui from openlp.plugins.presentations.lib.pdfcontroller import PdfController, PdfDocument from openlp.core.common import Settings +from openlp.core.common.path import Path from openlp.core.lib import ScreenList from tests.utils.constants import TEST_RESOURCES_PATH @@ -66,8 +67,8 @@ class TestPdfController(TestCase, TestMixin): self.desktop.screenGeometry.return_value = SCREEN['size'] self.screens = ScreenList.create(self.desktop) Settings().extend_default_settings(__default_settings__) - self.temp_folder = mkdtemp() - self.thumbnail_folder = mkdtemp() + self.temp_folder = Path(mkdtemp()) + self.thumbnail_folder = Path(mkdtemp()) self.mock_plugin = MagicMock() self.mock_plugin.settings_section = self.temp_folder @@ -77,8 +78,8 @@ class TestPdfController(TestCase, TestMixin): """ del self.screens self.destroy_settings() - shutil.rmtree(self.thumbnail_folder) - shutil.rmtree(self.temp_folder) + shutil.rmtree(str(self.thumbnail_folder)) + shutil.rmtree(str(self.temp_folder)) def test_constructor(self): """ @@ -98,7 +99,7 @@ class TestPdfController(TestCase, TestMixin): Test loading of a Pdf using the PdfController """ # GIVEN: A Pdf-file - test_file = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'pdf_test1.pdf') + test_file = Path(TEST_RESOURCES_PATH, 'presentations', 'pdf_test1.pdf') # WHEN: The Pdf is loaded controller = PdfController(plugin=self.mock_plugin) @@ -118,7 +119,7 @@ class TestPdfController(TestCase, TestMixin): Test loading of a Pdf and check size of generate pictures """ # GIVEN: A Pdf-file - test_file = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'pdf_test1.pdf') + test_file = Path(TEST_RESOURCES_PATH, 'presentations', 'pdf_test1.pdf') # WHEN: The Pdf is loaded controller = PdfController(plugin=self.mock_plugin) @@ -131,7 +132,7 @@ class TestPdfController(TestCase, TestMixin): # THEN: The load should succeed and pictures should be created and have been scales to fit the screen self.assertTrue(loaded, 'The loading of the PDF should succeed.') - image = QtGui.QImage(os.path.join(self.temp_folder, 'pdf_test1.pdf', 'mainslide001.png')) + image = QtGui.QImage(os.path.join(str(self.temp_folder), 'pdf_test1.pdf', 'mainslide001.png')) # Based on the converter used the resolution will differ a bit if controller.gsbin: self.assertEqual(760, image.height(), 'The height should be 760') diff --git a/tests/functional/openlp_plugins/presentations/test_pptviewcontroller.py b/tests/functional/openlp_plugins/presentations/test_pptviewcontroller.py index 3c08d226a..bfa74a7fa 100644 --- a/tests/functional/openlp_plugins/presentations/test_pptviewcontroller.py +++ b/tests/functional/openlp_plugins/presentations/test_pptviewcontroller.py @@ -22,7 +22,6 @@ """ This module contains tests for the pptviewcontroller module of the Presentations plugin. """ -import os import shutil from tempfile import mkdtemp from unittest import TestCase @@ -30,6 +29,7 @@ from unittest.mock import MagicMock, patch from openlp.plugins.presentations.lib.pptviewcontroller import PptviewDocument, PptviewController from openlp.core.common import is_win +from openlp.core.common.path import Path from tests.helpers.testmixin import TestMixin from tests.utils.constants import TEST_RESOURCES_PATH @@ -184,7 +184,7 @@ class TestPptviewDocument(TestCase): """ # GIVEN: mocked PresentationController.save_titles_and_notes and a pptx file doc = PptviewDocument(self.mock_controller, self.mock_presentation) - doc.file_path = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'test.pptx') + doc.file_path = Path(TEST_RESOURCES_PATH, 'presentations', 'test.pptx') doc.save_titles_and_notes = MagicMock() # WHEN reading the titles and notes @@ -201,13 +201,13 @@ class TestPptviewDocument(TestCase): """ # GIVEN: mocked PresentationController.save_titles_and_notes and an nonexistent file with patch('builtins.open') as mocked_open, \ - patch('openlp.plugins.presentations.lib.pptviewcontroller.os.path.exists') as mocked_exists, \ + patch.object(Path, 'exists') as mocked_path_exists, \ patch('openlp.plugins.presentations.lib.presentationcontroller.check_directory_exists') as \ mocked_dir_exists: - mocked_exists.return_value = False + mocked_path_exists.return_value = False mocked_dir_exists.return_value = False doc = PptviewDocument(self.mock_controller, self.mock_presentation) - doc.file_path = 'Idontexist.pptx' + doc.file_path = Path('Idontexist.pptx') doc.save_titles_and_notes = MagicMock() # WHEN: Reading the titles and notes @@ -215,7 +215,7 @@ class TestPptviewDocument(TestCase): # THEN: File existens should have been checked, and not have been opened. doc.save_titles_and_notes.assert_called_once_with(None, None) - mocked_exists.assert_any_call('Idontexist.pptx') + mocked_path_exists.assert_called_with() self.assertEqual(mocked_open.call_count, 0, 'There should be no calls to open a file.') def test_create_titles_and_notes_invalid_file(self): @@ -228,7 +228,7 @@ class TestPptviewDocument(TestCase): mocked_is_zf.return_value = False mocked_open.filesize = 10 doc = PptviewDocument(self.mock_controller, self.mock_presentation) - doc.file_path = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'test.ppt') + doc.file_path = Path(TEST_RESOURCES_PATH, 'presentations', 'test.ppt') doc.save_titles_and_notes = MagicMock() # WHEN: reading the titles and notes diff --git a/tests/functional/openlp_plugins/presentations/test_presentationcontroller.py b/tests/functional/openlp_plugins/presentations/test_presentationcontroller.py index c5e6d3df3..36ceb6f43 100644 --- a/tests/functional/openlp_plugins/presentations/test_presentationcontroller.py +++ b/tests/functional/openlp_plugins/presentations/test_presentationcontroller.py @@ -23,9 +23,8 @@ Functional tests to test the PresentationController and PresentationDocument classes and related methods. """ -import os from unittest import TestCase -from unittest.mock import MagicMock, mock_open, patch +from unittest.mock import MagicMock, call, patch from openlp.core.common.path import Path from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument @@ -40,7 +39,7 @@ class TestPresentationController(TestCase): def setUp(self): self.get_thumbnail_folder_patcher = \ patch('openlp.plugins.presentations.lib.presentationcontroller.PresentationDocument.get_thumbnail_folder', - return_value=Path()) + return_value=Path()) self.get_thumbnail_folder_patcher.start() mocked_plugin = MagicMock() mocked_plugin.settings_section = 'presentations' @@ -67,23 +66,18 @@ class TestPresentationController(TestCase): Test PresentationDocument.save_titles_and_notes method with two valid lists """ # GIVEN: two lists of length==2 and a mocked open and get_thumbnail_folder - mocked_open = mock_open() - with patch('builtins.open', mocked_open), patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder: + with patch('openlp.plugins.presentations.lib.presentationcontroller.Path.write_text') as mocked_write_text, \ + patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder: titles = ['uno', 'dos'] notes = ['one', 'two'] # WHEN: calling save_titles_and_notes - mocked_get_thumbnail_folder.return_value = 'test' + mocked_get_thumbnail_folder.return_value = Path('test') self.document.save_titles_and_notes(titles, notes) # THEN: the last call to open should have been for slideNotes2.txt - mocked_open.assert_any_call(os.path.join('test', 'titles.txt'), mode='wt', encoding='utf-8') - mocked_open.assert_any_call(os.path.join('test', 'slideNotes1.txt'), mode='wt', encoding='utf-8') - mocked_open.assert_any_call(os.path.join('test', 'slideNotes2.txt'), mode='wt', encoding='utf-8') - self.assertEqual(mocked_open.call_count, 3, 'There should be exactly three files opened') - mocked_open().writelines.assert_called_once_with(['uno', 'dos']) - mocked_open().write.assert_any_call('one') - mocked_open().write.assert_any_call('two') + self.assertEqual(mocked_write_text.call_count, 3, 'There should be exactly three files written') + mocked_write_text.assert_has_calls([call('uno\ndos'), call('one'), call('two')]) def test_save_titles_and_notes_with_None(self): """ @@ -107,10 +101,11 @@ class TestPresentationController(TestCase): """ # GIVEN: A mocked open, get_thumbnail_folder and exists - with patch('builtins.open', mock_open(read_data='uno\ndos\n')) as mocked_open, \ + with patch('openlp.plugins.presentations.lib.presentationcontroller.Path.read_text', + return_value='uno\ndos\n') as mocked_read_text, \ patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder, \ - patch('openlp.plugins.presentations.lib.presentationcontroller.os.path.exists') as mocked_exists: - mocked_get_thumbnail_folder.return_value = 'test' + patch('openlp.plugins.presentations.lib.presentationcontroller.Path.exists') as mocked_exists: + mocked_get_thumbnail_folder.return_value = Path('test') mocked_exists.return_value = True # WHEN: calling get_titles_and_notes @@ -121,45 +116,36 @@ class TestPresentationController(TestCase): self.assertEqual(len(result_titles), 2, 'There should be two items in the titles') self.assertIs(type(result_notes), list, 'result_notes should be of type list') self.assertEqual(len(result_notes), 2, 'There should be two items in the notes') - self.assertEqual(mocked_open.call_count, 3, 'Three files should be opened') - mocked_open.assert_any_call(os.path.join('test', 'titles.txt'), encoding='utf-8') - mocked_open.assert_any_call(os.path.join('test', 'slideNotes1.txt'), encoding='utf-8') - mocked_open.assert_any_call(os.path.join('test', 'slideNotes2.txt'), encoding='utf-8') - self.assertEqual(mocked_exists.call_count, 3, 'Three files should have been checked') + self.assertEqual(mocked_read_text.call_count, 3, 'Three files should be read') def test_get_titles_and_notes_with_file_not_found(self): """ Test PresentationDocument.get_titles_and_notes method with file not found """ # GIVEN: A mocked open, get_thumbnail_folder and exists - with patch('builtins.open') as mocked_open, \ - patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder, \ - patch('openlp.plugins.presentations.lib.presentationcontroller.os.path.exists') as mocked_exists: - mocked_get_thumbnail_folder.return_value = 'test' - mocked_exists.return_value = False + with patch('openlp.plugins.presentations.lib.presentationcontroller.Path.read_text') as mocked_read_text, \ + patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder: + mocked_read_text.side_effect = FileNotFoundError() + mocked_get_thumbnail_folder.return_value = Path('test') # WHEN: calling get_titles_and_notes result_titles, result_notes = self.document.get_titles_and_notes() # THEN: it should return two empty lists - self.assertIs(type(result_titles), list, 'result_titles should be of type list') + self.assertIsInstance(result_titles, list, 'result_titles should be of type list') self.assertEqual(len(result_titles), 0, 'there be no titles') - self.assertIs(type(result_notes), list, 'result_notes should be a list') + self.assertIsInstance(result_notes, list, 'result_notes should be a list') self.assertEqual(len(result_notes), 0, 'but the list should be empty') - self.assertEqual(mocked_open.call_count, 0, 'No calls to open files') - self.assertEqual(mocked_exists.call_count, 1, 'There should be one call to file exists') def test_get_titles_and_notes_with_file_error(self): """ Test PresentationDocument.get_titles_and_notes method with file errors """ # GIVEN: A mocked open, get_thumbnail_folder and exists - with patch('builtins.open') as mocked_open, \ - patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder, \ - patch('openlp.plugins.presentations.lib.presentationcontroller.os.path.exists') as mocked_exists: - mocked_get_thumbnail_folder.return_value = 'test' - mocked_exists.return_value = True - mocked_open.side_effect = IOError() + with patch('openlp.plugins.presentations.lib.presentationcontroller.Path.read_text') as mocked_read_text, \ + patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder: + mocked_read_text.side_effect = IOError() + mocked_get_thumbnail_folder.return_value = Path('test') # WHEN: calling get_titles_and_notes result_titles, result_notes = self.document.get_titles_and_notes() @@ -180,18 +166,16 @@ class TestPresentationDocument(TestCase): patch('openlp.plugins.presentations.lib.presentationcontroller.check_directory_exists') self.get_thumbnail_folder_patcher = \ patch('openlp.plugins.presentations.lib.presentationcontroller.PresentationDocument.get_thumbnail_folder') - self.os_patcher = patch('openlp.plugins.presentations.lib.presentationcontroller.os') self._setup_patcher = \ patch('openlp.plugins.presentations.lib.presentationcontroller.PresentationDocument._setup') self.mock_check_directory_exists = self.check_directory_exists_patcher.start() self.mock_get_thumbnail_folder = self.get_thumbnail_folder_patcher.start() - self.mock_os = self.os_patcher.start() self.mock_setup = self._setup_patcher.start() self.mock_controller = MagicMock() - self.mock_get_thumbnail_folder.return_value = 'returned/path/' + self.mock_get_thumbnail_folder.return_value = Path('returned/path/') def tearDown(self): """ @@ -199,7 +183,6 @@ class TestPresentationDocument(TestCase): """ self.check_directory_exists_patcher.stop() self.get_thumbnail_folder_patcher.stop() - self.os_patcher.stop() self._setup_patcher.stop() def test_initialise_presentation_document(self): @@ -227,7 +210,7 @@ class TestPresentationDocument(TestCase): PresentationDocument(self.mock_controller, 'Name') # THEN: check_directory_exists should have been called with 'returned/path/' - self.mock_check_directory_exists.assert_called_once_with(Path('returned', 'path')) + self.mock_check_directory_exists.assert_called_once_with(Path('returned', 'path/')) self._setup_patcher.start() From d801ca9b09010e75769c994ea4e102163e9d4647 Mon Sep 17 00:00:00 2001 From: Philip Ridout Date: Mon, 18 Sep 2017 07:20:06 +0100 Subject: [PATCH 07/13] Test patched which method --- openlp/core/lib/shutil.py | 12 +- openlp/core/ui/servicemanager.py | 5 +- .../presentations/lib/impresscontroller.py | 8 +- .../functional/openlp_core_lib/test_shutil.py | 153 ++++++++++-------- 4 files changed, 97 insertions(+), 81 deletions(-) diff --git a/openlp/core/lib/shutil.py b/openlp/core/lib/shutil.py index 44dea590a..1c7a9a393 100755 --- a/openlp/core/lib/shutil.py +++ b/openlp/core/lib/shutil.py @@ -95,19 +95,17 @@ def rmtree(*args, **kwargs): args, kwargs = replace_params(args, kwargs, ((0, 'path', path_to_str),)) return shutil.rmtree(*args, **kwargs) -# TODO: Test and tidy + + def which(*args, **kwargs): """ - Wraps :func:shutil.rmtree` so that we can accept Path objects. + Wraps :func:shutil.which` so that it return a Path objects. - :param openlp.core.common.path.Path path: Takes a Path object which is then converted to a str object - :return: Passes the return from :func:`shutil.rmtree` back - :rtype: None + :rtype: openlp.core.common.Path See the following link for more information on the other parameters: - https://docs.python.org/3/library/shutil.html#shutil.rmtree + https://docs.python.org/3/library/shutil.html#shutil.which """ - file_name = shutil.which(*args, **kwargs) if file_name: return str_to_path(file_name) diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index eb279f267..b393ad736 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -376,7 +376,10 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa self._file_name = path_to_str(file_path) self.main_window.set_service_modified(self.is_modified(), self.short_file_name()) Settings().setValue('servicemanager/last file', file_path) - self._save_lite = file_path.suffix() == '.oszl' + if file_path and file_path.suffix() == '.oszl': + self._save_lite = True + else: + self._save_lite = False def file_name(self): """ diff --git a/openlp/plugins/presentations/lib/impresscontroller.py b/openlp/plugins/presentations/lib/impresscontroller.py index 472e07801..e4b45465c 100644 --- a/openlp/plugins/presentations/lib/impresscontroller.py +++ b/openlp/plugins/presentations/lib/impresscontroller.py @@ -223,10 +223,9 @@ class ImpressDocument(PresentationDocument): if desktop is None: self.controller.start_process() desktop = self.controller.get_com_desktop() - url = self.file_path.as_uri() else: desktop = self.controller.get_uno_desktop() - url = uno.systemPathToFileUrl(str(self.file_path)) + url = self.file_path.as_uri() if desktop is None: return False self.desktop = desktop @@ -253,10 +252,7 @@ class ImpressDocument(PresentationDocument): if self.check_thumbnails(): return temp_folder_path = self.get_temp_folder() - if is_win(): - thumb_dir_url = temp_folder_path.as_uri() - else: - thumb_dir_url = uno.systemPathToFileUrl(str(temp_folder_path)) + thumb_dir_url = temp_folder_path.as_uri() properties = [] properties.append(self.create_property('FilterName', 'impress_png_Export')) properties = tuple(properties) diff --git a/tests/functional/openlp_core_lib/test_shutil.py b/tests/functional/openlp_core_lib/test_shutil.py index f502e403b..0f6ca9078 100755 --- a/tests/functional/openlp_core_lib/test_shutil.py +++ b/tests/functional/openlp_core_lib/test_shutil.py @@ -3,15 +3,15 @@ from unittest import TestCase from unittest.mock import ANY, MagicMock, patch from openlp.core.common.path import Path -from openlp.core.lib import shutilpatches +from openlp.core.lib.shutil import copy, copyfile, copytree, rmtree, which -class TestShutilPatches(TestCase): +class TestShutil(TestCase): """ Tests for the :mod:`openlp.core.lib.shutil` module """ - def test_pcopy(self): + def test_copy(self): """ Test :func:`copy` """ @@ -19,133 +19,152 @@ class TestShutilPatches(TestCase): with patch('openlp.core.lib.shutil.shutil.copy', return_value=os.path.join('destination', 'test', 'path')) \ as mocked_shutil_copy: - # WHEN: Calling shutilpatches.copy with the src and dst parameters as Path object types - result = shutilpatches.copy(Path('source', 'test', 'path'), Path('destination', 'test', 'path')) + # WHEN: Calling :func:`copy` with the src and dst parameters as Path object types + result = copy(Path('source', 'test', 'path'), Path('destination', 'test', 'path')) - # THEN: `shutil.copy` should have been called with the str equivalents of the Path objects. - # `shutilpatches.copy` should return the str type result of calling `shutil.copy` as a Path - # object. + # THEN: :func:`shutil.copy` should have been called with the str equivalents of the Path objects. + # :func:`copy` should return the str type result of calling :func:`shutil.copy` as a Path object. mocked_shutil_copy.assert_called_once_with(os.path.join('source', 'test', 'path'), os.path.join('destination', 'test', 'path')) self.assertEqual(result, Path('destination', 'test', 'path')) - def test_pcopy_follow_optional_params(self): + def test_copy_follow_optional_params(self): """ Test :func:`copy` when follow_symlinks is set to false """ # GIVEN: A mocked `shutil.copy` with patch('openlp.core.lib.shutil.shutil.copy', return_value='') as mocked_shutil_copy: - # WHEN: Calling shutilpatches.copy with `follow_symlinks` set to False - shutilpatches.copy(Path('source', 'test', 'path'), - Path('destination', 'test', 'path'), - follow_symlinks=False) + # WHEN: Calling :func:`copy` with :param:`follow_symlinks` set to False + copy(Path('source', 'test', 'path'), Path('destination', 'test', 'path'), follow_symlinks=False) - # THEN: `shutil.copy` should have been called with follow_symlinks is set to false + # THEN: :func:`shutil.copy` should have been called with :param:`follow_symlinks` set to false mocked_shutil_copy.assert_called_once_with(ANY, ANY, follow_symlinks=False) - def test_pcopyfile(self): + def test_copyfile(self): """ Test :func:`copyfile` """ - # GIVEN: A mocked `shutil.copyfile` which returns a test path as a string - with patch('openlp.core.lib.shutil.shutil.copyfile', return_value=os.path.join('destination', 'test', 'path')) \ - as mocked_shutil_copyfile: + # GIVEN: A mocked :func:`shutil.copyfile` which returns a test path as a string + with patch('openlp.core.lib.shutil.shutil.copyfile', + return_value=os.path.join('destination', 'test', 'path')) as mocked_shutil_copyfile: - # WHEN: Calling shutilpatches.copyfile with the src and dst parameters as Path object types - result = shutilpatches.copyfile(Path('source', 'test', 'path'), Path('destination', 'test', 'path')) + # WHEN: Calling :func:`copyfile` with the src and dst parameters as Path object types + result = copyfile(Path('source', 'test', 'path'), Path('destination', 'test', 'path')) - # THEN: `shutil.copyfile` should have been called with the str equivalents of the Path objects. - # `shutilpatches.copyfile` should return the str type result of calling `shutil.copyfile` as a Path + # THEN: :func:`shutil.copyfile` should have been called with the str equivalents of the Path objects. + # :func:`copyfile` should return the str type result of calling :func:`shutil.copyfile` as a Path # object. mocked_shutil_copyfile.assert_called_once_with(os.path.join('source', 'test', 'path'), os.path.join('destination', 'test', 'path')) self.assertEqual(result, Path('destination', 'test', 'path')) - def test_pcopyfile_optional_params(self): + def test_copyfile_optional_params(self): """ Test :func:`copyfile` when follow_symlinks is set to false """ - # GIVEN: A mocked `shutil.copyfile` + # GIVEN: A mocked :func:`shutil.copyfile` with patch('openlp.core.lib.shutil.shutil.copyfile', return_value='') as mocked_shutil_copyfile: - # WHEN: Calling shutilpatches.copyfile with `follow_symlinks` set to False - shutilpatches.copyfile(Path('source', 'test', 'path'), - Path('destination', 'test', 'path'), - follow_symlinks=False) + # WHEN: Calling :func:`copyfile` with :param:`follow_symlinks` set to False + copyfile(Path('source', 'test', 'path'), Path('destination', 'test', 'path'), follow_symlinks=False) - # THEN: `shutil.copyfile` should have been called with the optional parameters, with out any of the values - # being modified + # THEN: :func:`shutil.copyfile` should have been called with the optional parameters, with out any of the + # values being modified mocked_shutil_copyfile.assert_called_once_with(ANY, ANY, follow_symlinks=False) - def test_pcopytree(self): + def test_copytree(self): """ Test :func:`copytree` """ - # GIVEN: A mocked `shutil.copytree` which returns a test path as a string - with patch('openlp.core.lib.shutil.shutil.copytree', return_value=os.path.join('destination', 'test', 'path')) \ - as mocked_shutil_copytree: + # GIVEN: A mocked :func:`shutil.copytree` which returns a test path as a string + with patch('openlp.core.lib.shutil.shutil.copytree', + return_value=os.path.join('destination', 'test', 'path')) as mocked_shutil_copytree: - # WHEN: Calling shutilpatches.copytree with the src and dst parameters as Path object types - result = shutilpatches.copytree(Path('source', 'test', 'path'), Path('destination', 'test', 'path')) + # WHEN: Calling :func:`copytree` with the src and dst parameters as Path object types + result = copytree(Path('source', 'test', 'path'), Path('destination', 'test', 'path')) - # THEN: `shutil.copytree` should have been called with the str equivalents of the Path objects. - # `shutilpatches.copytree` should return the str type result of calling `shutil.copytree` as a Path - # object. + # THEN: :func:`shutil.copytree` should have been called with the str equivalents of the Path objects. + # :func:`patches.copytree` should return the str type result of calling :func:`shutil.copytree` as a + # Path object. mocked_shutil_copytree.assert_called_once_with(os.path.join('source', 'test', 'path'), os.path.join('destination', 'test', 'path')) self.assertEqual(result, Path('destination', 'test', 'path')) - def test_pcopytree_optional_params(self): + def test_copytree_optional_params(self): """ Test :func:`copytree` when optional parameters are passed """ - # GIVEN: A mocked `shutil.copytree` + # GIVEN: A mocked :func:`shutil.copytree` with patch('openlp.core.lib.shutil.shutil.copytree', return_value='') as mocked_shutil_copytree: mocked_ignore = MagicMock() mocked_copy_function = MagicMock() - # WHEN: Calling shutilpatches.copytree with the optional parameters set - shutilpatches.copytree(Path('source', 'test', 'path'), - Path('destination', 'test', 'path'), - symlinks=True, - ignore=mocked_ignore, - copy_function=mocked_copy_function, - ignore_dangling_symlinks=True) + # WHEN: Calling :func:`copytree` with the optional parameters set + copytree(Path('source', 'test', 'path'), Path('destination', 'test', 'path'), symlinks=True, + ignore=mocked_ignore, copy_function=mocked_copy_function, ignore_dangling_symlinks=True) - # THEN: `shutil.copytree` should have been called with the optional parameters, with out any of the values - # being modified - mocked_shutil_copytree.assert_called_once_with(ANY, ANY, - symlinks=True, - ignore=mocked_ignore, + # THEN: :func:`shutil.copytree` should have been called with the optional parameters, with out any of the + # values being modified + mocked_shutil_copytree.assert_called_once_with(ANY, ANY, symlinks=True, ignore=mocked_ignore, copy_function=mocked_copy_function, ignore_dangling_symlinks=True) - def test_prmtree(self): + def test_rmtree(self): """ Test :func:`rmtree` """ - # GIVEN: A mocked `shutil.rmtree` - with patch('openlp.core.lib.shutil.shutil.rmtree', return_value=None) as mocked_rmtree: + # GIVEN: A mocked :func:`shutil.rmtree` + with patch('openlp.core.lib.shutil.shutil.rmtree', return_value=None) as mocked_shutil_rmtree: - # WHEN: Calling shutilpatches.rmtree with the path parameter as Path object type - result = shutilpatches.rmtree(Path('test', 'path')) + # WHEN: Calling :func:`rmtree` with the path parameter as Path object type + result = rmtree(Path('test', 'path')) - # THEN: `shutil.rmtree` should have been called with the str equivalents of the Path object. - mocked_rmtree.assert_called_once_with(os.path.join('test', 'path')) + # THEN: :func:`shutil.rmtree` should have been called with the str equivalents of the Path object. + mocked_shutil_rmtree.assert_called_once_with(os.path.join('test', 'path')) self.assertIsNone(result) - def test_prmtree_optional_params(self): + def test_rmtree_optional_params(self): """ - Test :func:`rmtree` when optional parameters are passed + Test :func:`rmtree` when optional parameters are passed """ - # GIVEN: A mocked `shutil.rmtree` + # GIVEN: A mocked :func:`shutil.rmtree` with patch('openlp.core.lib.shutil.shutil.rmtree', return_value='') as mocked_shutil_rmtree: mocked_on_error = MagicMock() - # WHEN: Calling shutilpatches.rmtree with `ignore_errors` set to True and `onerror` set to a mocked object - shutilpatches.rmtree(Path('test', 'path'), ignore_errors=True, onerror=mocked_on_error) + # WHEN: Calling :func:`rmtree` with :param:`ignore_errors` set to True and `onerror` set to a mocked object + rmtree(Path('test', 'path'), ignore_errors=True, onerror=mocked_on_error) - # THEN: `shutil.rmtree` should have been called with the optional parameters, with out any of the values - # being modified + # THEN: :func:`shutil.rmtree` should have been called with the optional parameters, with out any of the + # values being modified mocked_shutil_rmtree.assert_called_once_with(ANY, ignore_errors=True, onerror=mocked_on_error) + + def test_which_no_command(self): + """ + Test :func:`which` when the command is not found. + """ + # GIVEN: A mocked :func:``shutil.which` when the command is not found. + with patch('openlp.core.lib.shutil.shutil.which', return_value=None) as mocked_shutil_which: + + # WHEN: Calling :func:`which` with a command that does not exist. + result = which('no_command') + + # THEN: :func:`shutil.which` should have been called with the command, and :func:`which` should return None. + mocked_shutil_which.assert_called_once_with('no_command') + self.assertIsNone(result) + + def test_which_command(self): + """ + Test :func:`which` when a command has been found. + """ + # GIVEN: A mocked :func:`shutil.which` when the command is found. + with patch('openlp.core.lib.shutil.shutil.which', + return_value=os.path.join('path', 'to', 'command')) as mocked_shutil_which: + + # WHEN: Calling :func:`which` with a command that exists. + result = which('command') + + # THEN: :func:`shutil.which` should have been called with the command, and :func:`which` should return a + # Path object equivalent of the command path. + mocked_shutil_which.assert_called_once_with('command') + self.assertEqual(result, Path('path', 'to', 'command')) From 0ee8ebb1c2f58cb4713d1c27456d6e0f01ba24f5 Mon Sep 17 00:00:00 2001 From: Philip Ridout Date: Mon, 18 Sep 2017 07:32:19 +0100 Subject: [PATCH 08/13] PEP fixes --- tests/functional/openlp_core_lib/test_shutil.py | 8 ++++---- .../presentations/test_presentationcontroller.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/functional/openlp_core_lib/test_shutil.py b/tests/functional/openlp_core_lib/test_shutil.py index 0f6ca9078..737f7ce00 100755 --- a/tests/functional/openlp_core_lib/test_shutil.py +++ b/tests/functional/openlp_core_lib/test_shutil.py @@ -13,7 +13,7 @@ class TestShutil(TestCase): def test_copy(self): """ - Test :func:`copy` + Test :func:`copy` """ # GIVEN: A mocked `shutil.copy` which returns a test path as a string with patch('openlp.core.lib.shutil.shutil.copy', return_value=os.path.join('destination', 'test', 'path')) \ @@ -43,7 +43,7 @@ class TestShutil(TestCase): def test_copyfile(self): """ - Test :func:`copyfile` + Test :func:`copyfile` """ # GIVEN: A mocked :func:`shutil.copyfile` which returns a test path as a string with patch('openlp.core.lib.shutil.shutil.copyfile', @@ -75,7 +75,7 @@ class TestShutil(TestCase): def test_copytree(self): """ - Test :func:`copytree` + Test :func:`copytree` """ # GIVEN: A mocked :func:`shutil.copytree` which returns a test path as a string with patch('openlp.core.lib.shutil.shutil.copytree', @@ -112,7 +112,7 @@ class TestShutil(TestCase): def test_rmtree(self): """ - Test :func:`rmtree` + Test :func:`rmtree` """ # GIVEN: A mocked :func:`shutil.rmtree` with patch('openlp.core.lib.shutil.shutil.rmtree', return_value=None) as mocked_shutil_rmtree: diff --git a/tests/functional/openlp_plugins/presentations/test_presentationcontroller.py b/tests/functional/openlp_plugins/presentations/test_presentationcontroller.py index 36ceb6f43..4cf2a1a01 100644 --- a/tests/functional/openlp_plugins/presentations/test_presentationcontroller.py +++ b/tests/functional/openlp_plugins/presentations/test_presentationcontroller.py @@ -39,7 +39,7 @@ class TestPresentationController(TestCase): def setUp(self): self.get_thumbnail_folder_patcher = \ patch('openlp.plugins.presentations.lib.presentationcontroller.PresentationDocument.get_thumbnail_folder', - return_value=Path()) + return_value=Path()) self.get_thumbnail_folder_patcher.start() mocked_plugin = MagicMock() mocked_plugin.settings_section = 'presentations' From 92c6b9c09dd2a5d93b1211d850c30165ac94b3af Mon Sep 17 00:00:00 2001 From: Philip Ridout Date: Mon, 18 Sep 2017 21:08:28 +0100 Subject: [PATCH 09/13] Revert some requested changes --- openlp/core/common/registry.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openlp/core/common/registry.py b/openlp/core/common/registry.py index 33afe6f21..1894ac458 100644 --- a/openlp/core/common/registry.py +++ b/openlp/core/common/registry.py @@ -138,8 +138,11 @@ class Registry(object): if result: results.append(result) except TypeError: + # Who has called me can help in debugging + trace_error_handler(log) log.exception('Exception for function {function}'.format(function=function)) else: + trace_error_handler(log) log.exception('Event {event} called but not registered'.format(event=event)) return results From b440584cb54a68200bd1d5bd53eea313c9a5b834 Mon Sep 17 00:00:00 2001 From: Philip Ridout Date: Wed, 20 Sep 2017 21:44:57 +0100 Subject: [PATCH 10/13] Moved the patched shuilils to the path module --- openlp/core/__init__.py | 3 +- openlp/core/common/path.py | 117 ++++++++++ openlp/core/lib/__init__.py | 29 --- openlp/core/lib/shutil.py | 112 ---------- openlp/core/ui/lib/filedialog.py | 3 +- openlp/core/ui/mainwindow.py | 3 +- .../presentations/lib/pdfcontroller.py | 3 +- .../lib/presentationcontroller.py | 3 +- .../openlp_core_common/test_path.py | 203 +++++++++++++++++- tests/functional/openlp_core_lib/test_lib.py | 34 +-- .../functional/openlp_core_lib/test_shutil.py | 170 --------------- 11 files changed, 325 insertions(+), 355 deletions(-) delete mode 100755 openlp/core/lib/shutil.py delete mode 100755 tests/functional/openlp_core_lib/test_shutil.py diff --git a/openlp/core/__init__.py b/openlp/core/__init__.py index 0fcea2d1a..8cd62a97f 100644 --- a/openlp/core/__init__.py +++ b/openlp/core/__init__.py @@ -38,10 +38,9 @@ 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.path import Path, copytree from openlp.core.common.versionchecker import VersionThread, get_application_version from openlp.core.lib import ScreenList -from openlp.core.lib.shutil import copytree from openlp.core.resources import qInitResources from openlp.core.ui import SplashScreen from openlp.core.ui.exceptionform import ExceptionForm diff --git a/openlp/core/common/path.py b/openlp/core/common/path.py index 3c4dd93c9..f11c4bb9f 100644 --- a/openlp/core/common/path.py +++ b/openlp/core/common/path.py @@ -19,6 +19,7 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### +import shutil from contextlib import suppress from openlp.core.common import is_win @@ -29,6 +30,121 @@ else: from pathlib import PosixPath as PathVariant +def replace_params(args, kwargs, params): + """ + Apply a transformation function to the specified args or kwargs + + :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. + :return: The modified positional and keyword arguments + :rtype: tuple[tuple, dict] + + + Usage: + Take a method with the following signature, and assume we which to apply the str function to arg2: + def method(arg1=None, arg2=None, arg3=None) + + As arg2 can be specified postitionally as the second argument (1 with a zero index) or as a keyword, the we + would call this function as follows: + + replace_params(args, kwargs, ((1, 'arg2', str),)) + """ + args = list(args) + for position, key_word, transform in params: + if len(args) > position: + args[position] = transform(args[position]) + elif key_word in kwargs: + kwargs[key_word] = transform(kwargs[key_word]) + return tuple(args), kwargs + + +def copy(*args, **kwargs): + """ + Wraps :func:`shutil.copy` so that we can accept Path objects. + + :param src openlp.core.common.path.Path: Takes a Path object which is then converted to a str object + :param dst openlp.core.common.path.Path: Takes a Path object which is then converted to a str object + :return: Converts the str object received from :func:`shutil.copy` to a Path or NoneType object + :rtype: openlp.core.common.path.Path | None + + See the following link for more information on the other parameters: + https://docs.python.org/3/library/shutil.html#shutil.copy + """ + + args, kwargs = replace_params(args, kwargs, ((0, 'src', path_to_str), (1, 'dst', path_to_str))) + + return str_to_path(shutil.copy(*args, **kwargs)) + + +def copyfile(*args, **kwargs): + """ + Wraps :func:`shutil.copyfile` so that we can accept Path objects. + + :param openlp.core.common.path.Path src: Takes a Path object which is then converted to a str object + :param openlp.core.common.path.Path dst: Takes a Path object which is then converted to a str object + :return: Converts the str object received from :func:`shutil.copyfile` to a Path or NoneType object + :rtype: openlp.core.common.path.Path | None + + See the following link for more information on the other parameters: + https://docs.python.org/3/library/shutil.html#shutil.copyfile + """ + + args, kwargs = replace_params(args, kwargs, ((0, 'src', path_to_str), (1, 'dst', path_to_str))) + + return str_to_path(shutil.copyfile(*args, **kwargs)) + + +def copytree(*args, **kwargs): + """ + Wraps :func:shutil.copytree` so that we can accept Path objects. + + :param openlp.core.common.path.Path src : Takes a Path object which is then converted to a str object + :param openlp.core.common.path.Path dst: Takes a Path object which is then converted to a str object + :return: Converts the str object received from :func:`shutil.copytree` to a Path or NoneType object + :rtype: openlp.core.common.path.Path | None + + See the following link for more information on the other parameters: + https://docs.python.org/3/library/shutil.html#shutil.copytree + """ + + args, kwargs = replace_params(args, kwargs, ((0, 'src', path_to_str), (1, 'dst', path_to_str))) + + return str_to_path(shutil.copytree(*args, **kwargs)) + + +def rmtree(*args, **kwargs): + """ + Wraps :func:shutil.rmtree` so that we can accept Path objects. + + :param openlp.core.common.path.Path path: Takes a Path object which is then converted to a str object + :return: Passes the return from :func:`shutil.rmtree` back + :rtype: None + + See the following link for more information on the other parameters: + https://docs.python.org/3/library/shutil.html#shutil.rmtree + """ + + args, kwargs = replace_params(args, kwargs, ((0, 'path', path_to_str),)) + + return shutil.rmtree(*args, **kwargs) + + +def which(*args, **kwargs): + """ + Wraps :func:shutil.which` so that it return a Path objects. + + :rtype: openlp.core.common.Path + + See the following link for more information on the other parameters: + https://docs.python.org/3/library/shutil.html#shutil.which + """ + file_name = shutil.which(*args, **kwargs) + if file_name: + return str_to_path(file_name) + return None + + def path_to_str(path=None): """ A utility function to convert a Path object or NoneType to a string equivalent. @@ -98,3 +214,4 @@ class Path(PathVariant): with suppress(ValueError): path = path.relative_to(base_path) return {'__Path__': path.parts} + diff --git a/openlp/core/lib/__init__.py b/openlp/core/lib/__init__.py index 1d55df497..4602fee2c 100644 --- a/openlp/core/lib/__init__.py +++ b/openlp/core/lib/__init__.py @@ -611,35 +611,6 @@ def create_separated_list(string_list): return list_to_string -def replace_params(args, kwargs, params): - """ - Apply a transformation function to the specified args or kwargs - - :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. - :return: The modified positional and keyword arguments - :rtype: tuple[tuple, dict] - - - Usage: - Take a method with the following signature, and assume we which to apply the str function to arg2: - def method(arg1=None, arg2=None, arg3=None) - - As arg2 can be specified postitionally as the second argument (1 with a zero index) or as a keyword, the we - would call this function as follows: - - replace_params(args, kwargs, ((1, 'arg2', str),)) - """ - args = list(args) - for position, key_word, transform in params: - if len(args) > position: - args[position] = transform(args[position]) - elif key_word in kwargs: - kwargs[key_word] = transform(kwargs[key_word]) - return tuple(args), kwargs - - from .exceptions import ValidationError from .screen import ScreenList from .formattingtags import FormattingTags diff --git a/openlp/core/lib/shutil.py b/openlp/core/lib/shutil.py deleted file mode 100755 index 1c7a9a393..000000000 --- a/openlp/core/lib/shutil.py +++ /dev/null @@ -1,112 +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 # -############################################################################### -""" Patch the shutil methods we use so they accept and return Path objects""" -import shutil - -from openlp.core.common.path import path_to_str, str_to_path -from openlp.core.lib import replace_params - - -def copy(*args, **kwargs): - """ - Wraps :func:`shutil.copy` so that we can accept Path objects. - - :param src openlp.core.common.path.Path: Takes a Path object which is then converted to a str object - :param dst openlp.core.common.path.Path: Takes a Path object which is then converted to a str object - :return: Converts the str object received from :func:`shutil.copy` to a Path or NoneType object - :rtype: openlp.core.common.path.Path | None - - See the following link for more information on the other parameters: - https://docs.python.org/3/library/shutil.html#shutil.copy - """ - - args, kwargs = replace_params(args, kwargs, ((0, 'src', path_to_str), (1, 'dst', path_to_str))) - - return str_to_path(shutil.copy(*args, **kwargs)) - - -def copyfile(*args, **kwargs): - """ - Wraps :func:`shutil.copyfile` so that we can accept Path objects. - - :param openlp.core.common.path.Path src: Takes a Path object which is then converted to a str object - :param openlp.core.common.path.Path dst: Takes a Path object which is then converted to a str object - :return: Converts the str object received from :func:`shutil.copyfile` to a Path or NoneType object - :rtype: openlp.core.common.path.Path | None - - See the following link for more information on the other parameters: - https://docs.python.org/3/library/shutil.html#shutil.copyfile - """ - - args, kwargs = replace_params(args, kwargs, ((0, 'src', path_to_str), (1, 'dst', path_to_str))) - - return str_to_path(shutil.copyfile(*args, **kwargs)) - - -def copytree(*args, **kwargs): - """ - Wraps :func:shutil.copytree` so that we can accept Path objects. - - :param openlp.core.common.path.Path src : Takes a Path object which is then converted to a str object - :param openlp.core.common.path.Path dst: Takes a Path object which is then converted to a str object - :return: Converts the str object received from :func:`shutil.copytree` to a Path or NoneType object - :rtype: openlp.core.common.path.Path | None - - See the following link for more information on the other parameters: - https://docs.python.org/3/library/shutil.html#shutil.copytree - """ - - args, kwargs = replace_params(args, kwargs, ((0, 'src', path_to_str), (1, 'dst', path_to_str))) - - return str_to_path(shutil.copytree(*args, **kwargs)) - - -def rmtree(*args, **kwargs): - """ - Wraps :func:shutil.rmtree` so that we can accept Path objects. - - :param openlp.core.common.path.Path path: Takes a Path object which is then converted to a str object - :return: Passes the return from :func:`shutil.rmtree` back - :rtype: None - - See the following link for more information on the other parameters: - https://docs.python.org/3/library/shutil.html#shutil.rmtree - """ - - args, kwargs = replace_params(args, kwargs, ((0, 'path', path_to_str),)) - - return shutil.rmtree(*args, **kwargs) - - -def which(*args, **kwargs): - """ - Wraps :func:shutil.which` so that it return a Path objects. - - :rtype: openlp.core.common.Path - - See the following link for more information on the other parameters: - https://docs.python.org/3/library/shutil.html#shutil.which - """ - file_name = shutil.which(*args, **kwargs) - if file_name: - return str_to_path(file_name) - return None diff --git a/openlp/core/ui/lib/filedialog.py b/openlp/core/ui/lib/filedialog.py index d4a702e83..0f3ef4058 100755 --- a/openlp/core/ui/lib/filedialog.py +++ b/openlp/core/ui/lib/filedialog.py @@ -22,8 +22,7 @@ """ Patch the QFileDialog so it accepts and returns Path objects""" from PyQt5 import QtWidgets -from openlp.core.common.path import Path, path_to_str, str_to_path -from openlp.core.lib import replace_params +from openlp.core.common.path import Path, path_to_str, replace_params, str_to_path class FileDialog(QtWidgets.QFileDialog): diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 88799b060..4b505b807 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -39,10 +39,9 @@ from openlp.core.api.http import server 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.path import Path, copyfile, path_to_str, str_to_path from openlp.core.common.versionchecker import get_application_version from openlp.core.lib import Renderer, PluginManager, ImageManager, PluginStatus, ScreenList, build_icon -from openlp.core.lib.shutil import copyfile from openlp.core.lib.ui import create_action from openlp.core.ui import AboutForm, SettingsForm, ServiceManager, ThemeManager, LiveController, PluginForm, \ ShortcutListForm, FormattingTagForm, PreviewController diff --git a/openlp/plugins/presentations/lib/pdfcontroller.py b/openlp/plugins/presentations/lib/pdfcontroller.py index a39cce36c..81fa3994a 100644 --- a/openlp/plugins/presentations/lib/pdfcontroller.py +++ b/openlp/plugins/presentations/lib/pdfcontroller.py @@ -27,9 +27,8 @@ 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.common.path import which from openlp.core.lib import ScreenList -from openlp.core.lib.shutil import which from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument if is_win(): diff --git a/openlp/plugins/presentations/lib/presentationcontroller.py b/openlp/plugins/presentations/lib/presentationcontroller.py index 3225eac24..304d70833 100644 --- a/openlp/plugins/presentations/lib/presentationcontroller.py +++ b/openlp/plugins/presentations/lib/presentationcontroller.py @@ -24,9 +24,8 @@ import logging 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.common.path import Path, rmtree from openlp.core.lib import create_thumb, validate_thumb -from openlp.core.lib.shutil import rmtree log = logging.getLogger(__name__) diff --git a/tests/functional/openlp_core_common/test_path.py b/tests/functional/openlp_core_common/test_path.py index f5abcffd5..0f35319c9 100644 --- a/tests/functional/openlp_core_common/test_path.py +++ b/tests/functional/openlp_core_common/test_path.py @@ -24,8 +24,209 @@ Package to test the openlp.core.common.path package. """ import os from unittest import TestCase +from unittest.mock import ANY, MagicMock, patch -from openlp.core.common.path import Path, path_to_str, str_to_path +from openlp.core.common.path import Path, copy, copyfile, copytree, path_to_str, replace_params, rmtree, str_to_path, \ + which + + +class TestShutil(TestCase): + """ + Tests for the :mod:`openlp.core.common.path` module + """ + def test_replace_params_no_params(self): + """ + Test replace_params when called with and empty tuple instead of parameters to replace + """ + # GIVEN: Some test data + test_args = (1, 2) + test_kwargs = {'arg3': 3, 'arg4': 4} + test_params = tuple() + + # WHEN: Calling replace_params + result_args, result_kwargs = replace_params(test_args, test_kwargs, test_params) + + # THEN: The positional and keyword args should not have changed + self.assertEqual(test_args, result_args) + self.assertEqual(test_kwargs, result_kwargs) + + def test_replace_params_params(self): + """ + Test replace_params when given a positional and a keyword argument to change + """ + # GIVEN: Some test data + test_args = (1, 2) + test_kwargs = {'arg3': 3, 'arg4': 4} + test_params = ((1, 'arg2', str), (2, 'arg3', str)) + + # WHEN: Calling replace_params + result_args, result_kwargs = replace_params(test_args, test_kwargs, test_params) + + # THEN: The positional and keyword args should have have changed + self.assertEqual(result_args, (1, '2')) + self.assertEqual(result_kwargs, {'arg3': '3', 'arg4': 4}) + + def test_copy(self): + """ + Test :func:`openlp.core.common.path.copy` + """ + # GIVEN: A mocked `shutil.copy` which returns a test path as a string + with patch('openlp.core.common.path.shutil.copy', return_value=os.path.join('destination', 'test', 'path')) \ + as mocked_shutil_copy: + + # WHEN: Calling :func:`openlp.core.common.path.copy` with the src and dst parameters as Path object types + result = copy(Path('source', 'test', 'path'), Path('destination', 'test', 'path')) + + # THEN: :func:`shutil.copy` should have been called with the str equivalents of the Path objects. + # :func:`openlp.core.common.path.copy` should return the str type result of calling + # :func:`shutil.copy` as a Path object. + mocked_shutil_copy.assert_called_once_with(os.path.join('source', 'test', 'path'), + os.path.join('destination', 'test', 'path')) + self.assertEqual(result, Path('destination', 'test', 'path')) + + def test_copy_follow_optional_params(self): + """ + Test :func:`openlp.core.common.path.copy` when follow_symlinks is set to false + """ + # GIVEN: A mocked `shutil.copy` + with patch('openlp.core.common.path.shutil.copy', return_value='') as mocked_shutil_copy: + + # WHEN: Calling :func:`openlp.core.common.path.copy` with :param:`follow_symlinks` set to False + copy(Path('source', 'test', 'path'), Path('destination', 'test', 'path'), follow_symlinks=False) + + # THEN: :func:`shutil.copy` should have been called with :param:`follow_symlinks` set to false + mocked_shutil_copy.assert_called_once_with(ANY, ANY, follow_symlinks=False) + + def test_copyfile(self): + """ + Test :func:`openlp.core.common.path.copyfile` + """ + # GIVEN: A mocked :func:`shutil.copyfile` which returns a test path as a string + with patch('openlp.core.common.path.shutil.copyfile', + return_value=os.path.join('destination', 'test', 'path')) as mocked_shutil_copyfile: + + # WHEN: Calling :func:`openlp.core.common.path.copyfile` with the src and dst parameters as Path object + # types + result = copyfile(Path('source', 'test', 'path'), Path('destination', 'test', 'path')) + + # THEN: :func:`shutil.copyfile` should have been called with the str equivalents of the Path objects. + # :func:`openlp.core.common.path.copyfile` should return the str type result of calling + # :func:`shutil.copyfile` as a Path object. + mocked_shutil_copyfile.assert_called_once_with(os.path.join('source', 'test', 'path'), + os.path.join('destination', 'test', 'path')) + self.assertEqual(result, Path('destination', 'test', 'path')) + + def test_copyfile_optional_params(self): + """ + Test :func:`openlp.core.common.path.copyfile` when follow_symlinks is set to false + """ + # GIVEN: A mocked :func:`shutil.copyfile` + with patch('openlp.core.common.path.shutil.copyfile', return_value='') as mocked_shutil_copyfile: + + # WHEN: Calling :func:`openlp.core.common.path.copyfile` with :param:`follow_symlinks` set to False + copyfile(Path('source', 'test', 'path'), Path('destination', 'test', 'path'), follow_symlinks=False) + + # THEN: :func:`shutil.copyfile` should have been called with the optional parameters, with out any of the + # values being modified + mocked_shutil_copyfile.assert_called_once_with(ANY, ANY, follow_symlinks=False) + + def test_copytree(self): + """ + Test :func:`openlp.core.common.path.copytree` + """ + # GIVEN: A mocked :func:`shutil.copytree` which returns a test path as a string + with patch('openlp.core.common.path.shutil.copytree', + return_value=os.path.join('destination', 'test', 'path')) as mocked_shutil_copytree: + + # WHEN: Calling :func:`openlp.core.common.path.copytree` with the src and dst parameters as Path object + # types + result = copytree(Path('source', 'test', 'path'), Path('destination', 'test', 'path')) + + # THEN: :func:`shutil.copytree` should have been called with the str equivalents of the Path objects. + # :func:`openlp.core.common.path.copytree` should return the str type result of calling + # :func:`shutil.copytree` as a Path object. + mocked_shutil_copytree.assert_called_once_with(os.path.join('source', 'test', 'path'), + os.path.join('destination', 'test', 'path')) + self.assertEqual(result, Path('destination', 'test', 'path')) + + def test_copytree_optional_params(self): + """ + Test :func:`openlp.core.common.path.copytree` when optional parameters are passed + """ + # GIVEN: A mocked :func:`shutil.copytree` + with patch('openlp.core.common.path.shutil.copytree', return_value='') as mocked_shutil_copytree: + mocked_ignore = MagicMock() + mocked_copy_function = MagicMock() + + # WHEN: Calling :func:`openlp.core.common.path.copytree` with the optional parameters set + copytree(Path('source', 'test', 'path'), Path('destination', 'test', 'path'), symlinks=True, + ignore=mocked_ignore, copy_function=mocked_copy_function, ignore_dangling_symlinks=True) + + # THEN: :func:`shutil.copytree` should have been called with the optional parameters, with out any of the + # values being modified + mocked_shutil_copytree.assert_called_once_with(ANY, ANY, symlinks=True, ignore=mocked_ignore, + copy_function=mocked_copy_function, + ignore_dangling_symlinks=True) + + def test_rmtree(self): + """ + Test :func:`rmtree` + """ + # GIVEN: A mocked :func:`shutil.rmtree` + with patch('openlp.core.common.path.shutil.rmtree', return_value=None) as mocked_shutil_rmtree: + + # WHEN: Calling :func:`openlp.core.common.path.rmtree` with the path parameter as Path object type + result = rmtree(Path('test', 'path')) + + # THEN: :func:`shutil.rmtree` should have been called with the str equivalents of the Path object. + mocked_shutil_rmtree.assert_called_once_with(os.path.join('test', 'path')) + self.assertIsNone(result) + + def test_rmtree_optional_params(self): + """ + Test :func:`openlp.core.common.path.rmtree` when optional parameters are passed + """ + # GIVEN: A mocked :func:`shutil.rmtree` + with patch('openlp.core.common.path.shutil.rmtree', return_value='') as mocked_shutil_rmtree: + mocked_on_error = MagicMock() + + # WHEN: Calling :func:`openlp.core.common.path.rmtree` with :param:`ignore_errors` set to True and + # :param:`onerror` set to a mocked object + rmtree(Path('test', 'path'), ignore_errors=True, onerror=mocked_on_error) + + # THEN: :func:`shutil.rmtree` should have been called with the optional parameters, with out any of the + # values being modified + mocked_shutil_rmtree.assert_called_once_with(ANY, ignore_errors=True, onerror=mocked_on_error) + + def test_which_no_command(self): + """ + Test :func:`openlp.core.common.path.which` when the command is not found. + """ + # GIVEN: A mocked :func:`shutil.which` when the command is not found. + with patch('openlp.core.common.path.shutil.which', return_value=None) as mocked_shutil_which: + + # WHEN: Calling :func:`openlp.core.common.path.which` with a command that does not exist. + result = which('no_command') + + # THEN: :func:`shutil.which` should have been called with the command, and :func:`which` should return None. + mocked_shutil_which.assert_called_once_with('no_command') + self.assertIsNone(result) + + def test_which_command(self): + """ + Test :func:`openlp.core.common.path.which` when a command has been found. + """ + # GIVEN: A mocked :func:`shutil.which` when the command is found. + with patch('openlp.core.common.path.shutil.which', + return_value=os.path.join('path', 'to', 'command')) as mocked_shutil_which: + + # WHEN: Calling :func:`openlp.core.common.path.which` with a command that exists. + result = which('command') + + # THEN: :func:`shutil.which` should have been called with the command, and :func:`which` should return a + # Path object equivalent of the command path. + mocked_shutil_which.assert_called_once_with('command') + self.assertEqual(result, Path('path', 'to', 'command')) class TestPath(TestCase): diff --git a/tests/functional/openlp_core_lib/test_lib.py b/tests/functional/openlp_core_lib/test_lib.py index 8b46e99c3..96e78e351 100644 --- a/tests/functional/openlp_core_lib/test_lib.py +++ b/tests/functional/openlp_core_lib/test_lib.py @@ -32,7 +32,7 @@ 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 + get_text_file_string, image_to_byte, resize_image, str_to_bool, validate_thumb TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'resources')) @@ -636,38 +636,6 @@ class TestLib(TestCase): thumb_path.stat.assert_called_once_with() self.assertFalse(result, 'The result should be False') - def test_replace_params_no_params(self): - """ - Test replace_params when called with and empty tuple instead of parameters to replace - """ - # GIVEN: Some test data - test_args = (1, 2) - test_kwargs = {'arg3': 3, 'arg4': 4} - test_params = tuple() - - # WHEN: Calling replace_params - result_args, result_kwargs = replace_params(test_args, test_kwargs, test_params) - - # THEN: The positional and keyword args should not have changed - self.assertEqual(test_args, result_args) - self.assertEqual(test_kwargs, result_kwargs) - - def test_replace_params_params(self): - """ - Test replace_params when given a positional and a keyword argument to change - """ - # GIVEN: Some test data - test_args = (1, 2) - test_kwargs = {'arg3': 3, 'arg4': 4} - test_params = ((1, 'arg2', str), (2, 'arg3', str)) - - # WHEN: Calling replace_params - result_args, result_kwargs = replace_params(test_args, test_kwargs, test_params) - - # THEN: The positional and keyword args should have have changed - self.assertEqual(result_args, (1, '2')) - self.assertEqual(result_kwargs, {'arg3': '3', 'arg4': 4}) - def test_resize_thumb(self): """ Test the resize_thumb() function diff --git a/tests/functional/openlp_core_lib/test_shutil.py b/tests/functional/openlp_core_lib/test_shutil.py deleted file mode 100755 index 737f7ce00..000000000 --- a/tests/functional/openlp_core_lib/test_shutil.py +++ /dev/null @@ -1,170 +0,0 @@ -import os -from unittest import TestCase -from unittest.mock import ANY, MagicMock, patch - -from openlp.core.common.path import Path -from openlp.core.lib.shutil import copy, copyfile, copytree, rmtree, which - - -class TestShutil(TestCase): - """ - Tests for the :mod:`openlp.core.lib.shutil` module - """ - - def test_copy(self): - """ - Test :func:`copy` - """ - # GIVEN: A mocked `shutil.copy` which returns a test path as a string - with patch('openlp.core.lib.shutil.shutil.copy', return_value=os.path.join('destination', 'test', 'path')) \ - as mocked_shutil_copy: - - # WHEN: Calling :func:`copy` with the src and dst parameters as Path object types - result = copy(Path('source', 'test', 'path'), Path('destination', 'test', 'path')) - - # THEN: :func:`shutil.copy` should have been called with the str equivalents of the Path objects. - # :func:`copy` should return the str type result of calling :func:`shutil.copy` as a Path object. - mocked_shutil_copy.assert_called_once_with(os.path.join('source', 'test', 'path'), - os.path.join('destination', 'test', 'path')) - self.assertEqual(result, Path('destination', 'test', 'path')) - - def test_copy_follow_optional_params(self): - """ - Test :func:`copy` when follow_symlinks is set to false - """ - # GIVEN: A mocked `shutil.copy` - with patch('openlp.core.lib.shutil.shutil.copy', return_value='') as mocked_shutil_copy: - - # WHEN: Calling :func:`copy` with :param:`follow_symlinks` set to False - copy(Path('source', 'test', 'path'), Path('destination', 'test', 'path'), follow_symlinks=False) - - # THEN: :func:`shutil.copy` should have been called with :param:`follow_symlinks` set to false - mocked_shutil_copy.assert_called_once_with(ANY, ANY, follow_symlinks=False) - - def test_copyfile(self): - """ - Test :func:`copyfile` - """ - # GIVEN: A mocked :func:`shutil.copyfile` which returns a test path as a string - with patch('openlp.core.lib.shutil.shutil.copyfile', - return_value=os.path.join('destination', 'test', 'path')) as mocked_shutil_copyfile: - - # WHEN: Calling :func:`copyfile` with the src and dst parameters as Path object types - result = copyfile(Path('source', 'test', 'path'), Path('destination', 'test', 'path')) - - # THEN: :func:`shutil.copyfile` should have been called with the str equivalents of the Path objects. - # :func:`copyfile` should return the str type result of calling :func:`shutil.copyfile` as a Path - # object. - mocked_shutil_copyfile.assert_called_once_with(os.path.join('source', 'test', 'path'), - os.path.join('destination', 'test', 'path')) - self.assertEqual(result, Path('destination', 'test', 'path')) - - def test_copyfile_optional_params(self): - """ - Test :func:`copyfile` when follow_symlinks is set to false - """ - # GIVEN: A mocked :func:`shutil.copyfile` - with patch('openlp.core.lib.shutil.shutil.copyfile', return_value='') as mocked_shutil_copyfile: - - # WHEN: Calling :func:`copyfile` with :param:`follow_symlinks` set to False - copyfile(Path('source', 'test', 'path'), Path('destination', 'test', 'path'), follow_symlinks=False) - - # THEN: :func:`shutil.copyfile` should have been called with the optional parameters, with out any of the - # values being modified - mocked_shutil_copyfile.assert_called_once_with(ANY, ANY, follow_symlinks=False) - - def test_copytree(self): - """ - Test :func:`copytree` - """ - # GIVEN: A mocked :func:`shutil.copytree` which returns a test path as a string - with patch('openlp.core.lib.shutil.shutil.copytree', - return_value=os.path.join('destination', 'test', 'path')) as mocked_shutil_copytree: - - # WHEN: Calling :func:`copytree` with the src and dst parameters as Path object types - result = copytree(Path('source', 'test', 'path'), Path('destination', 'test', 'path')) - - # THEN: :func:`shutil.copytree` should have been called with the str equivalents of the Path objects. - # :func:`patches.copytree` should return the str type result of calling :func:`shutil.copytree` as a - # Path object. - mocked_shutil_copytree.assert_called_once_with(os.path.join('source', 'test', 'path'), - os.path.join('destination', 'test', 'path')) - self.assertEqual(result, Path('destination', 'test', 'path')) - - def test_copytree_optional_params(self): - """ - Test :func:`copytree` when optional parameters are passed - """ - # GIVEN: A mocked :func:`shutil.copytree` - with patch('openlp.core.lib.shutil.shutil.copytree', return_value='') as mocked_shutil_copytree: - mocked_ignore = MagicMock() - mocked_copy_function = MagicMock() - - # WHEN: Calling :func:`copytree` with the optional parameters set - copytree(Path('source', 'test', 'path'), Path('destination', 'test', 'path'), symlinks=True, - ignore=mocked_ignore, copy_function=mocked_copy_function, ignore_dangling_symlinks=True) - - # THEN: :func:`shutil.copytree` should have been called with the optional parameters, with out any of the - # values being modified - mocked_shutil_copytree.assert_called_once_with(ANY, ANY, symlinks=True, ignore=mocked_ignore, - copy_function=mocked_copy_function, - ignore_dangling_symlinks=True) - - def test_rmtree(self): - """ - Test :func:`rmtree` - """ - # GIVEN: A mocked :func:`shutil.rmtree` - with patch('openlp.core.lib.shutil.shutil.rmtree', return_value=None) as mocked_shutil_rmtree: - - # WHEN: Calling :func:`rmtree` with the path parameter as Path object type - result = rmtree(Path('test', 'path')) - - # THEN: :func:`shutil.rmtree` should have been called with the str equivalents of the Path object. - mocked_shutil_rmtree.assert_called_once_with(os.path.join('test', 'path')) - self.assertIsNone(result) - - def test_rmtree_optional_params(self): - """ - Test :func:`rmtree` when optional parameters are passed - """ - # GIVEN: A mocked :func:`shutil.rmtree` - with patch('openlp.core.lib.shutil.shutil.rmtree', return_value='') as mocked_shutil_rmtree: - mocked_on_error = MagicMock() - - # WHEN: Calling :func:`rmtree` with :param:`ignore_errors` set to True and `onerror` set to a mocked object - rmtree(Path('test', 'path'), ignore_errors=True, onerror=mocked_on_error) - - # THEN: :func:`shutil.rmtree` should have been called with the optional parameters, with out any of the - # values being modified - mocked_shutil_rmtree.assert_called_once_with(ANY, ignore_errors=True, onerror=mocked_on_error) - - def test_which_no_command(self): - """ - Test :func:`which` when the command is not found. - """ - # GIVEN: A mocked :func:``shutil.which` when the command is not found. - with patch('openlp.core.lib.shutil.shutil.which', return_value=None) as mocked_shutil_which: - - # WHEN: Calling :func:`which` with a command that does not exist. - result = which('no_command') - - # THEN: :func:`shutil.which` should have been called with the command, and :func:`which` should return None. - mocked_shutil_which.assert_called_once_with('no_command') - self.assertIsNone(result) - - def test_which_command(self): - """ - Test :func:`which` when a command has been found. - """ - # GIVEN: A mocked :func:`shutil.which` when the command is found. - with patch('openlp.core.lib.shutil.shutil.which', - return_value=os.path.join('path', 'to', 'command')) as mocked_shutil_which: - - # WHEN: Calling :func:`which` with a command that exists. - result = which('command') - - # THEN: :func:`shutil.which` should have been called with the command, and :func:`which` should return a - # Path object equivalent of the command path. - mocked_shutil_which.assert_called_once_with('command') - self.assertEqual(result, Path('path', 'to', 'command')) From b4a687c85e102088a7f77ad1536942561627fa81 Mon Sep 17 00:00:00 2001 From: Philip Ridout Date: Thu, 21 Sep 2017 07:20:56 +0100 Subject: [PATCH 11/13] PEP fixes --- openlp/core/common/path.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openlp/core/common/path.py b/openlp/core/common/path.py index f11c4bb9f..cdb115940 100644 --- a/openlp/core/common/path.py +++ b/openlp/core/common/path.py @@ -214,4 +214,3 @@ class Path(PathVariant): with suppress(ValueError): path = path.relative_to(base_path) return {'__Path__': path.parts} - From 131e0213e2c93b938a539491e18fca2bf76b1866 Mon Sep 17 00:00:00 2001 From: Philip Ridout Date: Thu, 21 Sep 2017 08:40:41 +0100 Subject: [PATCH 12/13] minor fix --- openlp/plugins/presentations/lib/mediaitem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/plugins/presentations/lib/mediaitem.py b/openlp/plugins/presentations/lib/mediaitem.py index d9a14e0ed..8061bb193 100644 --- a/openlp/plugins/presentations/lib/mediaitem.py +++ b/openlp/plugins/presentations/lib/mediaitem.py @@ -331,7 +331,7 @@ class PresentationMediaItem(MediaManagerItem): file_path = str_to_path(bitem.data(QtCore.Qt.UserRole)) path, file_name = file_path.parent, file_path.name service_item.title = file_name - if file_path.exists: + if file_path.exists(): if self.display_type_combo_box.itemData(self.display_type_combo_box.currentIndex()) == 'automatic': service_item.processor = self.find_controller_by_type(file_path) if not service_item.processor: From 3e05a64874447d3a6ebf3eaf022b7ac51755fcb1 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Fri, 22 Sep 2017 05:03:28 -0700 Subject: [PATCH 13/13] PJlink2 update K --- documentation/PJLink_Notes.odt | Bin 29323 -> 29407 bytes documentation/PJLink_Notes.pdf | Bin 398704 -> 403913 bytes openlp/core/lib/projector/db.py | 4 +- openlp/core/lib/projector/pjlink.py | 110 +++++++++++------- openlp/core/lib/projector/pjlink2.py | 85 -------------- openlp/core/ui/projector/manager.py | 14 +-- .../openlp_core_lib/test_projector_db.py | 2 +- .../test_projector_pjlink_base.py | 5 +- .../test_projector_pjlink_cmd_routing.py | 6 +- .../test_projector_pjlink_commands.py | 6 +- 10 files changed, 88 insertions(+), 144 deletions(-) delete mode 100644 openlp/core/lib/projector/pjlink2.py diff --git a/documentation/PJLink_Notes.odt b/documentation/PJLink_Notes.odt index dcf71cdf2388b190cd4fdfaa1fec4914e8428ad5..f65a2b2b49200eeec1ead404bdfc4a910f7e5fc1 100644 GIT binary patch delta 22838 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 diff --git a/documentation/PJLink_Notes.pdf b/documentation/PJLink_Notes.pdf index d7dc9a084bc75763f77667b657a724da6cae18a2..78b992e23852c0761506debc4a2b23d1ed131e7b 100644 GIT binary patch delta 143930 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�lQcS=HaS=+LL zmpLTIHkoc;o^JR2ve_BKZx4RzJvVA!Zs9vl`2JG<3k`d#6gTn>R8Jc4(ro|wNuloW ztrN>KHqe^if9~B-7pFe_VLhLT*^?LA-^IR1C(@Sw3>)5h<`itu zy~pa>;Fm+Y>?Y%SVVCw|tE@1;qp7{~oeK=yD`vkj%9^+&bB9Gj<@b-XRabKBp{bjk z$HC9!3!?Q)F6_D*>DMlcxfarH0zJ{VX#tZijw?$eJ{57p65ZCQ++gB!rILq4EgEF8 zqp4R#9j@-v$7fC*CniuSe@WEg^8S{+$q&thdAKNTYG0FAn#!_D@}^jH$YN^1lK*Jn z?s4^VpYm(C&ymB9Nsim`a;;t5zmh!HlX^aa5utnKV@ajJfd9$}8KbG0iS2&v#w8Qo zYX(-;OB^`WHLb}*)BJ3=Rv6`h`6&+WaOuQQ2udCuIxwA@d;hkGrQ<`R+Q+=bg zEM?c?xaVa1pzG2?{nREcKdXxy&h7V1NTiKhymQCG zk+t9*?c#-xEwnr5lWVmS&eO|ir_O~I(@M{ZU!rX~d-({>`s}B4E$=h*{Z1v#Z)a&e_eG&$F~B=z}uwLr~MEi)H^3vP!Ntx%t!?WFC!!suj3xrDQgO#Io9#6uaH zMu`_nYJM&ksPP?CJ>u86dSA}fYq~Xao_42A+0L^4G50ca$^q50;>nMbOt;R-wQ(x% zG@ac+AJ3ok)K=euJA7)**}~Pg)}JxH9y`=(uf#fW^ChwmRx&U7{weHR--DL}%_kmR z&0M+QRO-D}lLJ8^f6F41O zCL6o4&8USpsJ?>Lu2y#&D_I((w=vQ=H|%HigW~>y1nIIA!(R zD2|SwGd$R9ux+r&Gj>q;xSvt$rCPp=7xT|8Y+sWtxb?Ba<>G0l0=lCTlhfbkrr&rz z96V62T{pMlgmAoz$F07Z9~a8vFqot0v>1NL14QC~Xt1T^eY0d) z=5s=Hk_R=`h4maSE;TAm_Vhx5dhhbEUYgxdwR{)U$eUxh~RQSQ$e z)@s6P8I^)Fxre8n6+e$|_KbfTr~g2E&c^$i4J(8Oo2Y$7qOmD{SEO%V_6sZYKk1!G z*LJtc^zmr-$fTP!uJGP#eE4mH6xWc)?kD^_y~i3Y{8T4>U8i=Vy|Yhp`hNKjJ^Xp;j_&pYm zzN0rIVEoyh!*edZ%ynFI#6M;2K*z1*$6T!Zq5i=WTgWg9;oFi8l!YHd)%U-5(X`OQ}F(^6Zr!sXt3zg0DzkmLO# z)Ou)#G)q7G%_Lh2&!tk`)h!EeZ?M!4x8J?`Nv2|+z?UUP^7G#ymjJmXsX||yUhFDz z+7{*3x8B5QTZHW0ilC-rhowGlSaANNZ*q3YzKU@gk6$h5k?0HBaBq6YnaV3~4Wl<@ z-#ssrx^uy{wE#=2Z;zhPHq*i{T@%(Fo1m-HdUR1v0)f@k>tPeB#qICAX*GS2Yio!o93(GFI^KUd-6CvNU&bqB2Syt=( z6Q6JA51gv*d2NpBmCX%0v?b5E1l?FV%hP&B*!5P2sMWQvItKMqc-rS3$=y5r4Zou@ z!EDI_?~uTnn$@hnkcTCK+tsJ4PO0{TF3r6Hb-Jy ztklY!x2s+XJ;=^ZkuA2q^q$3U)q8D7!?&Z#wS2>qq^%_Z-maDs#oIQ-Zzu`qFq#?x z$>WO1l`xm>o6KS*#LjNNx#RU@@gI}bew5^Q$@|>vbA4U3V)ne((z!9+^{#h+dZ%5V z{$tYd)2>g63R+x;$>bEYG$ShWSDkCX;FQ0=OJf5DyP&SHW(Z7U{Hmy#MvPz644ItQ zzo!{8>126q?W8$Dhe;nPi)n`R--S3;?^hxJdmg+@j#E}+xw|L$Toa8Q$kOYa=C!xS zm~U5k(E4rMU%Yb#heST=r5_L+Z<6cVnAq8td9v|Tn9&NO`;Yy~LVXqHsV#c2*fX`M z`|iNcCnZJEkzb$g?fv=ja%t?R?4dgu<>nQ)o?Tk@^W}@4p|&3LUd3aX1By>sU*Gk7 zD^BL!dU>u=^Y>5NzO#zduWiFJSi|>se8`YlMeGLPGv)TZ?f z-~VCKxKU9h(0)bClkwN&nQu?Lw>KGo{&dNe@5?h$UN>_?ZsF>gj(tWu_eFVyG$y+_ zrnFc;g&*^tI!ZLI{Nk2yV*K&;PzklqWt}J0^Nwp=+k5qd#x(>#PBwR^N?6OyE`O~t zw=2GKj{OQt#}K8}){Y^%Ecj`Y^?2UOJzCp0);Xp$aU~~bH0?azIsM6=zX;doh{pTsS5b;o6U(@h6XI*X}&ZdeCD8rO-P`Zi5Bt^B-S-!hrvM~1bQ zPYk2?G~Uk8o7HvVJ;SH#)O!ZYrziY}&>P8C1;yO_7X!~qUJT?UEj8;;^k3IsZ+-A_ zjgQi!Q0FDb431x&xpGV6xftDr&dV=2xb#$QM{5c;9_gREh39SRJ?j0bsp#Zlt>pPx zDYA2dQ-&lw10Al7SCE)KPc<{p8K267#9p7tLfH!nJlgilWvV(`UpN$4qG!z#TlU1z z#%G31ng#`lU34ql6_!_RU%*#N z<8S(K=H^S|*$W+9kLl}DOL!~93eL4jML3;sqpsxANO5kOn7ON;`Br07a1GbJ2$yZI zXtN(QT;3@wceib?rsUSerPIptcn6oU9$s-wS&-+uxA9OK%kfo%)8x6lZfP@LgwB%> zyt~TBNO0%sh*Q`F!K11LS911@V~KAyHDSg6tbo!}NIZ+4_ImHn*!Ww+eV?}dyt$>XXQtF6rQA1B zk{hQz$3yu&go|_LA2%?5eStoF)^69;+bOM7<&Lf$+Si`GlJgA>#m_%$pSiYvrAqda zSKJRbQ7@iZ_44GcmNWi(=RC^Qs&+e6Kg~Fo%zAOL zgUo{G_Zb?M7rsA!@$h!*-oTX|GcR57PVJZyt*mBvWNOHoEj{m+)(sthUikT{VEN!U zQ@2gy@{8iR(ZZMGM1_rgaPgLtS;HpUWxO_#H53&enJrVLecT>SpMLa$0zDlwUd$!> z>Zf41J8MGMOeYtIJK2ZZBIxz6H-+7xKPpX_QbbuVY1kn>kf-PeJ;+tAUD8_Q!Loxf5|5B)R$g=~fro3|{cRRkE;KblCixpvL5aoozn6o~D*6fsz}) zG&=6wZoF1sCgnKm#wi`DLYM!91J#E{4D7 z=aQ>Go)@3Sn-FWe&eWwUZ>=dyBmR)`?Bd7aJAy1M=eE>_iLKisxO4jbFBb~(<2FR* zytyrPa%IT}(ddo^ukZA435gmnHTAM+|LF)7_{k9f7kSi2>1_e|#PUmE&g zT*Y@=mx*VJ{fpP$8akUT9q(?v;f=ZS=`~vuCEEGr4(!<>@T!)3K?<{q0;oL8ZG9J9YrmFvQcq|`i>=7>42umJUlT;ngs(b#T2F4Hv0axF zcT1m^ z>B_2c;=In zW{%$|?<;=q?vzgxRK@&uEaz6dqZ0F2@7e7V)gIsedPxJr;5qHe>%W~jV(!OU?mdt> zuq-i3qvpwq?XO?%+VHZ_<{fqJ^rJdeXIyyiW*;wzs{gWJN@O{Iw7|Z!tq=DWNjW7h zL654-y%#JVJay1u+l&>*mtXIBygR|>L(~L|v+>sNOlJ4;YMh*0 zVw5<{J2{%mr*Yn@<=JsJ$5%&tIc+Ly634F1on%hga7V;`T;%GH-zOgsK6Cl7TWhOR{POzZ zwW-3_H_lRQseYe-ZD>)y<9Ctg_15c-YxWpYb_jWMw+DulEIn$~zdYcemC^%C``JgH z;XTtMWaJI|Zwr^_J^p&JgYS;uD|%z^|RqSkCfnsYV9ent@uV!w}S8mI_A!zq8LS5d@ zBRO}|3zHvHly3;_=^aep%x}^@>;1#xO24bFmAnDrt*w&pHTO036tM2LyUvZoDx3H9 z$ybKf6;}7EhTE9OoG%j+YA-B2_cAZ*-ucs8TzOONbcSXpAL0p77g&E59e-e5yM#A2 z>tuBT{mhJz>sRi0=2?f@b>CZL6yo1s2tEO^0>-r)@I1`9AxnyJn0` zhwIdD+8NWh?lp;jZ`Rpc$Le`VdGP+$b)h}QJp#|nmmYJS7&k@w!{S*rv~jD?pH95p z>Jr_0;P&|yLZ$9Kg{uOJ75D?@84gI@S9|N1_*S28Yx2R^Cd?$Ht;(et>v%WO+%`0| z?9)Ky0lwL5?yCFCR2jF&mmK{*;n{9aYxVbs^QyuecX*U+|6(8767l7T?rg>SD@Lv7 ztUF$m+=>WY@O9=X{%yURTKb-TXJI|39yO4Ccv(Fvr`=66V*cI^P}!Y$E-Jef&wAkI zx>DX+B(}|ynwTH=Fs4p>;)D%0%9y#K$K6?XHz`ypmtWqYV)L#P$ zR1RG54fv_>W%2#)3^nWTdwW^ehO@6-U*%yM$h9E4>Yde@l7p4YS+@>-$^9YH`Q*_8 zX;Hxfr}Ns0(JglKq?VK)GSH){t^2X|R`J|bu71xB><)Y4`NQSlt=bPS)+c6opER~y zFE@7zYyJ12h#giG$BFZ9ya}opypn3O_0D=ZArBu-#kSR*SLU5s;jk=h>zk>;TZ<;F zs93uGyYNc{FP}~=H#A{b}BxyhINUHwcXXAX5XKVYD8kbG)8*f-t~onylh=+~X*9R&ri0@$=}XVVoF|%ldX&Vj9}-l_6z=T}X0^5bWY*PX9Jy{T z>|1J{k+8?gVgGW~!mK$%DzqjD_#0-YeiOeNFZ>ESWl+E8&WGy~nvX|Sm z&P&Z^j|}@Z3C1Pt6g`#Gt}GQ8WuB*h&R}w}Xwq73_r`O#QmXZj`AXn!>jU!XkMlai zq{3LYhLY7}Piu}ZSv}3w>u8*P^PVAzQ|scml!Nh^%bzcrO`Yd(_i(L721{vk%f-dH zG&Gw-Z`6_M7Qj($o$;vwW+%9nq>aZmQA8r=EYx| z<&K`?wM`7YWhv@WCU~(SwV*33RxF<|o)pMIA z`0Cx8T~{fW_f=eXXM=Pmgve{Cs`V_I%Ux;p9lONISa&qS;CjuCZFv?rVLQ_DG9f+;ru(KUd=g zIoa*St{0fS$|4Pm0Py+jx2Mp{DWMqS&;{Go=nrRS=_~UbCFkfL*q;e_c-N- z;GhX_wJN`LT}#b7y1O1P4FA4A)$5D2mudWQ~naB&blbUBK2T-?Eg|o5ebpf!b$SjQ`pU2Qp*zZA+P)a~&%R%$ zKHk&3cR%Clj*Y?b%i@;A^PhYgG@~J$H)pP5+NTh+)?Ihb@A8((V#H2s`@rj-lKa%3 zt8Yo6Mg!e1CA`-(GS%pizOr1vj*{*>=LdTkYnB{h%pFBT}slOIrIyG8Na?s>_PFcf?Fka+r6t<$+hQsyoF! zuxqPp!^X!uxmL2`ov*}QeJE`vw!)HYMdag)7kJ$o#<_Vcmww1~F%Vn5dXAZL(@x$d z=i@8IH4d%To|$@f>8IE&hf6Axbss(`8sAV>?Phm9q&h{gx^t&)u}#JP@vEe6D5xCT zGH_p%5uas!EOUvKy~n!*)2O-Mr|J(q&8gk5U!U95v^(Q+d=z}Dx}8;0aSyt@ zL_Z#LOXZrn{@##`gJ<5jvk#MHEtb8Gm>YJZ_apV(?1V=R?V2-v1};*iCeGJ8GW)bdO=r-YeC*-mDEMe#NlIiNchj@7B>F9wDtqg1MVN`AE#ltn3uNGov)@ zZFuR2S2@lB8A}S*@>@-g@$;J2Br~p{(LY?SF{0*?w)MD+ovL)VSI0Y=n`;%cBdr#7 zyl$ML(ER4sc5ki=p_Qt7`4j+#hkLZ4AJx0|(;SCGl6W2pL6J(T@!lwb_#M-ul5(zkkH zI(nS;D4qWMasZW~$7!aHBG1rcUk|X|-o&G;XD}icm0>V?;xBnePc&en4*?h@f1V09 zK}Vipz~s!O8Zc?Um0}p^qwF|L9UXAQ68IS;DJKCm-;_rt-_i+_#JLy-hDa#@)5%X* zi0NWLi4fCCKzCvAqM+qI!M=VJ5QmTqrbV0a$c*P0gRw7ECRwL@d~Wa_Z5P~7FZuAR zNvL=_k0>gQ!=#g2U+t1IAP{pBm_B`!K!&tj*6RcuX) z#V}-}f%GR~edY|qe*wdg4aQidIIb~TDx+j9RGL62Uo;TgEG@(^;#}>}r!i>A#gZ-8 zaejs&8z(eu31-xWIll5<~4B~!ZO(EI?*cM`+sUsN|T zNM89`jf=|E`=#1eX{??vd14p8DAh=RRCUkPqp}?@je>2(8Ab?G!Nk!#32c16u`)Il zL%tFq6~QibGUg)rPwkj`G^8wvDbBK@f(IHB!S1AMMT2J}BEaF9PP`v^W>7$=k!L0` zW@NlRJcEM+A#n+gTY!zMf`h_{84P5%8Jj6drVJ-tC%@3(@DLxrP~syda!kN9QSD|d z@b90?v1SAX1&4*Y3s4z!(uly1L73Tzcp$YR9!NtG52T311BpNJKuARnCh;cTkp4wH zkU9_#9LnUKBw{dm0f~4}AP-f@S0rhWgUKPyWFmYk#`+KGC4WnAz>(E}hHh`g;{HBM z6K&px+2ZU^B%%H3AV1I=FzFl|H+ZH12YjL zfgC;205xpG6#t8(#!k$Hyp`oNY95ox_@4|LU?LA`LR7PqO9AO5U_yUq%rQKnMjc9@@T_Q=|)H~;jb3O5-~qsJu(Q9 z*8BeQ~46h87h zKLs-*{{BMzh6rV*V&*vDw=NYk=As)Jp)3ZEBuPPx#-K8-44DRo&~2f+L)pPZUdMsF zKpG~5>lq-~G)!)S9y>kSdWVLg@=e$zVCi@Wqo9a1%vpu3L!5N&{36I4N$Ne?a7;ZT z^dSvfO&uBN5aPFv$e3)JBh#oPN@J!$(FD_!&?SV;cK>58$%Mm_`e(|ZGRarO1x`rb zvx37Rdoy&cmWPBA5>mY2|Cq^Mzc@3=tTKH% za##q~8%l$@chfN&w)-e@MC33f*^CuYpRLf^Lzp?+#gzZVgoOE*mZZmzC1JqU68Nej z%flE2+{FJdru>KT$ZM~^{Dqx$#?m1?<8Q`eTb^S)BlP?*7RzGCp{|Ro8-;4b&@rSK z=rMG_%u970yhHrhYd0ycE(ZYiUnAJj{>5ShpD{Afe>o71^i_@n(P$LZmVreQ@#b~} zvdq6|L>qDC3r8?@#wcOw`V_b{p##86(d?s`9ue5< zj$(?`kqINJjY!8)jdb=g@X|(Hmn8J*D5gPV28CmoyviSh{ux%Z5kHPPhE3(<5h-;2 z7&cySY=E*c;P?vdpYBFOq6Z*>OgfHPJ=_bFtA z&&M%SoTiV|H$zN)KZeU9%6&`_T|WsF_n*K_C1|91$802+XvQI~DWe9OoWx9dX=D;2 zlASDyeg^vLUM3)0=}C}H^)zP2M<<~Z zKye6!|GtEKw19VE08R%Rtiz>oCh(BQ>1bgl=84nkNK_q{hLELtfv9@xaXBP>2Ajo4 zC-DF|109{Y6$ zyat6n3_@O@P`GJ`aSpS>>4qr!92O2A#Gl3Fa42Ae&y(YneQ^mcCPNR^U&myL_xp(V z3`G5ai{p?o9hY%a(yn8oNaF&KVNlVU3s^WxID$x~4P1hO!i;mBAjdY4T~&`?22z}Aq8f^skiqD90E5z^zkz0fHSG6;Q|1sbLy9XdT3oP<^ZN5^g|7&Fby^(r7Hv_ zItsV~vH)%*ez2YSb_KYuea0<`lrlNig4eNmBoW6VAX1yjA)o9ibzNI}73|WOK=);ZoSS=@t-FjL`Z55C!TAwkQTj;2HtI zN&ukm96`hY?SVJAKB=T6Qr^X-htvvTu09Q|E(8hm+16v|rxk$!`bfDJoW1h`F;^dL zhIh#6A~qSd=5nbaxng1>6ZsSalJ72Z@M190mtu0Vs2NOtbcvYEL{TN=J5nR}EWo8^ z08>Mv*9jVi$n`qVXv+c`yUv0&z)iZZvjqU}MAg{@4F+0M3hxX^&qQ`Pm=Jm&4)F#E z_}~Uq}rO_wowoIyts<)FgMTu{N(4syxFn*g5j4UoZrftKF@ z8Cr6G=|MxoH%JPk$y%-u6lf@}0w^SX1sizgfflCHxI7ZNiFxA&259X~EQ|~4s}T{X zi2D|1k3-5OZPs~}pg=|2;U#j+$5ha&MvR;+k7&2C*<4IRJrsEx3q$Ynfimfq5SA0~ z!0;J^V8gBgOc94SiNu@40eBO14M3RB1;$Xni&+8zItso^$Y40)j!+Kcl@J~J#Y%OSOTat4`6k!?Bf*lo$q%W~*Cd`6JYnug1u)^f-SBsGr)atc}e zq52yz1q`c>-Zo(FAdvy!j}gf_XD{#|^*Gju%?7F1Ys#Yvwp1|dJ}H$EQjWr9;bp^4 zE^TDdhDo8e``Bz;55lbpR<0Gdz{`faU^}KqtY+JrKq%;n)HH!R_S}Np07A(k?FZOw zUOl$r?YH5@>IMh~i%jU2fMAo+;|G{Au1815-f_ziQBSpl3`t72<{@SY71`PcF!akE zjKoC&O>D+y!P_Jjw+woI7uM70JV@*QmEc35tpLhy#sVj@S7rg-dG1t0WM2tf-&bN2 z$W)K|F2nd0k4P%u(h>+&fnC9N3W)bHp@I>b_ZXXol&fKxslN}!OW|W|Hp;5TOprq` z7dJ9{f-S+RR78p3l0#RYV5iWk8W;-Rq>Qqj!b(rn+JwV!*8!OEfB9%2DyNu8FjGY%qij)L0em)drwwSl9=w>DxRxMMqD=G6$VNrrT1*g%%G zLu!JWufZMs>1!iLYLgDkQkY%Okx0kL+JS?F0?W{V%YaX!8S*K<@>yUFsxWGtodNyw-gPNF;W` zYoS-52GmT>ui$m%LpBQ^oMwOyY~_~AuYZk|V`ybFxZwOl5PKG{K@#9Ej9$DY=oupI zE|_?*8N62SHJFq9Z00-SGb5Dn4n8w`1e2FU;99?g7e>drVE|n(zpI<*;Z-~Wm>2he zu5>E$+{`77))B72)r(n5&`FmjaG}#FQ1H;1U>EKufL-510RBTJR9SjFfM;S4Hjx*` zKpPCre~Qf@eToRfM($~-{TU|B_Hf=70Az13CW17%aMC2ALhP-$fbI+mRi9Hl9c|&5 zr>Y-z()ozZ5F9fT*=B+0_gf*_yuLxWzW9i#tB_ejK-VDvXtT>i zHXAY-kmiQKC&DB68H6%PdCRANP)^JS%3^Imxla=pMrZ;qgi41%3FCfDU2qH!l+*^) zHul4Aub>SgjwC7n1ye?G!pqfrorQII!VY(7jKDn^#4D z3=;xnHeT$WB&i6hzX@LOwH;E3*JrT#(>?+?51a?S(Maw>|Ai}QFE$KhZ4M}rJ2{xR z7K)<79RLat6GT~GF!M2nf+havJA4wF^_-3OROB;&O;H&k_$S`%{wT@F0Kpn9frg(0 zI3(%*fj>#ZVaGH`Ebg5I7-Ip?9K_UR{>H=ry&i;^DtgI=KoBK-gFo50j~M(Pf+34R z$eXgS37~)Be){_e?yBsb^1mQQ!Ze}~QI?~`w`_&9Nz5UajlmpIx-bbbD*NPyLS-M% zz;Cwm^>(r6PDFA;n8g^Jp~!W-!Ziua@8>Ls}SLRboi#6f!YU%8EVJi$%G3<{{UJ`PBaiLf9%oAC$AzvF`4*(B}5K9izQ z*@p}iDsmvo`WYXQQ39UKP^I(_#eQVEk6ATw+PyT;p(bm=>OJ$-Ek*1fHDi~ z0U+Oh;;;aLm3nxES0P zgHGiaOmY%Ctw9`+Aq_TS6F@V5LjH{Y0)@d$7F4f9`WG9WzuEH;-vFkAygqSHM!7t= z(U`c=H$XoP#N> z2#t&zvjQ2QBfQWX$Rvif^WsWq5kEdQX~NE@p&KeX^KY0gvKPS>5Z`!QO_j!ObN|zK z41TS?G?a6TO9H+A1`$WnK0h8e|AYH6e&D`w2uM4Kz)okW0InwYchU@WSO8?y8Rkg) z(TbY}Z+A3crfGmf-5!Rj)ItE3?kf{;)&G$ESM-A90G9*L=z<__F>$n$pr7x#<`6?S!pg@Eb@ViHMpl?a%cG69#6Ww&|%X(U4`5}kv~q#njcR;wRUu=P4S3Ffuy z!Y$5ps&e2hQ72o{?`@T5V}L+AZrabs6bXqh2r>(F~J8X`P5zrnKe9s ziAX0%{ap;4J6Hmr^0ztwcCwcwfT|QPhpLF=A5@L>P&_}#TEGjka!L$&bW;-Y{g~T7c9a${W!TH5Jm0FhAi1#4-ooCXb{i{atw@I&zr=Viplo zNU8#|9A9I%X8-AHMhql^fQ{dHf(=m`fwRe>%lo*M*v>{BBmM92H5v(P!r9`RAj^vg z;FC#bD-b5wgR=?$;cO^L0G#q0Xh7_NI_^M1FBSmn$KB-?MIwg4c9S%|0EunpQbCD^ zxX{=N+h}B21BUDXpwJMd76=ARV0$@$nzV@PUvm4%MxA>i4%3gemt19`Mp5jPwyB``=(00zoL0wEbN zl-LZ8iU4h6MJPS0M1TP~yIu)5B$zvj;u1V?zW~bX08w0zJ<(PbS4w-1$&OwE4cME1 zBre8=NgkOg`;iLx=Tji-QG*;U-UGBod(G@EI+1QU#{~XHn^YFIr%6 z)SL{Ahg5zsW}mZjL_|8O@G+JPMFp#wo9C~LAXQMi;y)usQYI?*AAMm7Oa&3jB(p0g?u zsE}Av;Cz>$3%`-~Oc+}u!S)0>6uBK5HZyMx z2Q8rsQ-Pd>9EY5cHa-)|;UqZ_{F@Un)ft( zrr;{VYQa~AwQwgIGDj4c_LK(}$2<1?T z=)&^O0Ab&&0g7bBRK&n%jP?UM9Na}gZjPBUM%Y!%{~*dl7nnd)OPvsb zN}RIk;}&CxLO+Y>Z#Jj{tgwe2ajsH*T;s2j8l)Kj%@_?1&ErP!Ct33v{LcJiECDB! zVvDIfMqJK10{M63ULCk6dAv1w>WB=pjByjuQ9;UXtf7*roGPTv z7&nHT;HV8bVU99%G{Uq&cXDChXaZczjBvRz1jtZEBGWho?#>5<{+WH@yp7zb-knAm z%!H_BX2D7X&1w@CnY^pi2lvl(g5U|3M9OZHD-aCX&tX)E5jb5t%X6eifMpa4?%?4p3-NIYh-Q z#u9~Y)*2u1&N?a(B@2G!Ri_xTSEKqih438E@1Ser(oV??B5 zhTuaI*5E@;hU^TdfP$?5AVy>F?3Wq=HPS34RC=&y4Iqdcb8`F* zn?Hy$pc@8me#n^ZLmDV_4z8j6H+sJ|J}9SxTMC(*aMmNKxnr}tJ`$M&F4SlOHux=T z<=p>})d2C&16gBD+16G;E9U(nt08;Ot!f4Ua9l_pDcJr8Q6sd(7Kk>QaflAvfua^G zz=hcN-#BYGvY!qvR2jo1j3TD9T}T6YlY?z=zOm*?Gl!cabU4oQ76!9TF^5W=xF>RZ z_WyLD-#sYw-4Y(awdi(_6d% zS5x5}GC~B6;l^o!!yy~;o`g#wT_R}!I%4Sc0?2ct_!_d$i5^-KGWkL=0@Mv;|4NvK zo-TyNWTO=fC+WpI17*^eXune!2$N{yk5g(V$s>Jd93I=kmVp(Dp3sKR1w zAWkWZVAmqH7}D}+2*~3h88mecu1g@Sh;A;1HRN}tGM3RuIYMAt-V91MK(b&HUgYv8 zwq!vXI~VBw@P*ts!4;N|ztM&Bb64O!Wgg@yQqGTVBhbpSk3s*bG<2Pay9TIn9&qql z3d6A{xT-Lnecg>S9y!?JlHkW=(Z9hK9ITq~Kmtyov2daeTU z8lif3&~m9g+efD&)>6RnZ-n(x=~7$~H-MFC8MG$?JRy1s2UX{keE$&TmqlSy3`g~m z4zLyGIIJRyUpC^aYH(Ifo|FEAz=%mf7r5d>Cd6ruz}k#3;J?9q%n_Fqg-fBNn~rSY ziQ=6|gEt)kLlO92GicBg`p>`p6k5qYJYYQuaRRCwkS7xHDvHi|0U*D-Afp}D5blx9!77%(6giXDn#2-zV#;)_OEMNmnCIL~@=lvf<;TFO&AZoXeL-di)2+p$XZ>n&- z?^g~qgs;_*1>o0(z=ne_nGnR#7k`)y34u>=j26gi5x6dceW-bG5gh#Fc#L4m80)&D z*o@>M!W!&TAo|F+X#ir<)9B710GYi4eaCnjKpgA?5VsRajRUbFlJ^_4x{gKH5V`w- z?2(IM3Q6;V-6vWWQSb|F^{A?FA#Wu zD@%@Q6^xxsMP+r+rCjbh3SmXGd?m~y!5zaC)w=??aLJSC!wa|pxRZTgAmOmIzk$nX z2NUA4kJjkyucAPS^9-t?lWrVgTLQo%K_SV z?jTj%T5w#k>u`sm-UFAE<6~-aKShTl7xKKO z!gG`f6cAU?nDd%3+On5YQtFZRi-#8&m}Hh3G&YMs7m7Tt;qzugKNZ;qLjw7IelV6Y z1MMO}JK@cNUMGYD{lBk}hHz#O1kl?P`S>^V$!kZTSJnR;tKSI$F17kXIrQ-uWccx` zKoi2*|C&igu9p)wV`EC8kDM>VH;~CoK0AB}cPDNOCa=NGkT{W=p=On^9F<);i!o4P z7dudykQ&0mN)Cf-P$Lz~$T#}vBi}bdj;#<0tLDYmY31 zIKm35mv|Pq#MxIP<+$B}@42(lOYE3kJ)n<7*T5PxauH1ohIN5lp16Wz>37pAjM64}Webjt|^99)~hgIkEeXkP~R zeK+qQm=P<%$hWu26=V#fku*iD1m%{Hzc+>v;X>?8rfdN9+jl_~G-5d-g&P>Ag~KSR zjkppBLUwF4!trTn%|^&%KQ{pcu#FXl|EI)WIoVTwucGi>5$KS~qMPAB8)_O^G=TKJJZ~Y&fNK*q#m~dt*km&zE*1kO6#_EgvbI*AWx~@C8 zE_W~-^BDKc=a`2Ip=d5C&2wlX4VOxkIk8NMW+GH7Ns%a#s8m!!DQQ6UD^a}PwfEZR zJkPoJx!(8ldHW;hp0kIw*RO(sbg57p5vo;8tf`Zyf$XO#HCUF4;X2v{!5FGS0ciXqcA% z3qhK24;-mO6Cs>`j`K8a>M{=o7j=c9ktxiXp|N{GZ#DK$k21ZdA{V68h0CK+Wi{=+ z7aLaY1kifzcyDEsC0!XfBFdc@f1g>%m~2M!6V0~;Ub}}t?%xK8c-6}c7mt5J+Z~cOMy5}f`{-L_S|U&sQM%jkk_6aNS#r! z#>gE78lUnTXxx4jH({yTI~kuwJr9S~mNdhS*P=1st57!V58Pe>8dZpeAhmW`YbuNs z)+eEU46=3&sPoupcy4niIaLX6h;T{{^>_d?sD%!g>}g>OGdtIrB00Q4WLf?20Pv## zo4N^!6}d0Kf;9%AlP`g!sIt<$_(Xg5LC!&TKUJLuyfFfrn4I=<{Zgby>Ii$1A3lh{ z{>2l8>PAj=KkE9Br`9RqfHo8ZWa;Hx6i(ys z=Jf{Xo+(%m>r*#2NM${RC2E9QK8g)KfUctN9(SSv8BZ$9=ZdkOel&ABwoaSt;#ft5 zzMKwQHESG3xszngwg@Wo?&xvAO8e>9H=i5lIg9$vO0Lsw)D($z=xM+s7=GTglPxkS z*I{kZ`msnQ+18`r^(r&r14|ROg5s1z*d?BL)YF$%&Ga-YHV2yN9Ea%Bv6)z8&+&j_ z=4`#lTE%jUvE06gq|+-7fwFtXd(Pr$6lWDHvbv6MBkSRBAVw1=fT7=IcBqV|x(_}`6_(0%_n8>O~xs#ce3Yfy5VUdY7=d$8rvbIbq#i0NmIc$ zvz~MyGJ!bL)We`}^T$|nmtbQ&Yd*Ngb*vywnvbzQdkkZJJ3no#5Z$sB>2xZGoq0wsSz*GmUour06h-sE4EXp{<{ z#)N&QoAJJQ2A_DlD$;R}a)iVTL!ZJ7e{+}qA#7SB7eO$Y!<#xmPSQT1Ya=LD#X)Aq zGHiTPW&j;^6q7-3xop)5V=iE;UI`B4i?>yMwWtLFUAQBb#l~R)UH>dEmdS5Rvas(| z5XoWB0OrqFLn+(v&>BP&i=T4_{%o2pffJugo&#oXorx|A(=R@!OtXVCG2=hGagJRy z3+smU*P5RCMa#G&st1`GH4AMQJ`el#m04IWF8$ce$OQ6-j>jQEeP+YN0^#e^GcO=U zVT%kpAWH4FIA3K0>#x}m)X2iLL2$Vk3ClI{t2T0*LDUCtS}GuPH|P5*jJ8y~Rtfo2 zK_S|-$m92h5eib=(`23&{kn9|VnDAP1A92gjyHsZ{Cgp{VT_LpE9xD6%t9##%phsAcud<^Pcs`#kk z0!YF0oxn$@FEHePZK*Rv9ka4`NE zL;enm-6|o!*)Spf-z;ZC9r-WMdOB#Gzk$J3gjdDibY_)9=uwnlE`$5=vzqW(bl_!} zi+4XK-qVamxDou!tDY839EQ13TX3Bc`ap_5AQSLa0G`kH_7wB>SmERigt4dYf+}DA zJgagJ6>M$j&7%unfZfCXr<$$FYn9HXNX)avXy|L0&ASkES@)Wg!XcWn0u+8?At>B# zKGxE|G3M#H$Ue`VuX|dQy01$7&xAfH5{D4^;%rSHS!5iks&6}jRQFDE%f1CC0pP9G8+*M zUIH#sH@zQO0t2Go%RDx?D9!Lc!OiBa^fchI0=S=3Kuupl%hFYzb8S{2tfD?|Vo}?E z@V2Ej)u9~zS_OwkRBbsZZH8&Xw_vH=uoT3`komNIH5RM(O?{`wfNQu37@^6lFjK)Y zhL@eaMem64G_hr&I5^lOn?i$N4J`97EkhqQ;R!6QqIH)p_Y|BWDFf)GHi}D@LsMMy zw$eLmmt&Y6&q42Gz6{`%lUMW(Yy{gqEC&#{8F$G$m}?m$demC1)w5RCT5N34bVMQ1 z_SWaIPaNYj(vN@3sHs={%m%Y!*U3^kGog*yX|DkGhO33hE)Gs2lG+?~sX#xfQ7Edn zeHC3(w!&j;@uyN7iHg0C1PWEu`vI&t;#I)@+de?748|VsBBtiT7pY5gm{n=|ycJ;k z?e97TC_>+^1qZyif}^->I6k@#0^~Mkf<9aaJRe;F5ik4GQ&Z)g6u;+i*b|5#ZCk{< z{cEgs*npg}4R*wq_uL1@9-6%#1Gj!%a#=20pEf33!?{4fr>`?}vkjTQ!F}yU@gu<+ zaTJ!bA#cdQH9uf-|C^DYgHnT95HzFEp*O&U3;VJWwGxx4z2vozggLU!oeOC^dL=Ah z46b4yn^uAy&RrwykS6n8-^8&0+kmYC&6O*3&6^lhQNREDFvFhx01F58vr8hT20%LN z;{MyRd02h+Dgb6|#audnBT(SNF{ywOrd0yBaFwK*{jd?aQYohsKGWe3zycSravOXU zq1w40f>JKyB98R+hXDBCTQCsXY;sZw>G#Vz1J1Fl@uMR8?oE{pg-axy-V=Hot%q(- z8!ANi&&N=k-Zmy<(~sO&Yt%6aE}^6m?||8xd@RH+K@BoHu~QllNk{$xVAk9o83PXdcZeb3PVA4Xy}{_zZ9j+ki15p9`sr zBlct{yS0im=TA8o;C(~st9B_$Bd+H*gM1HgS^t0mK6qdB$X#1bLVL62g7!t*;U?m& zpHrcRegIQOk^cS9lcd*4Kjle}t#Ugc&fExefb>~({TH5?J&DP_JUGE`d;xU4vJqrD z70WbK~ zZ@@zixKa{p_DJR-6yd5(V?F|lum8#uabpzJkqP{R#fP^5_qlJuMZf&(N?ONh$x*ca za*JH5JNC6F>N+&?qXxsCfAC}QV6PuRQQljMBerW)Hp2zC=VQX#YOi^SOIHy5S4#r>(!jpZ=Ymsy!;i5vECo?;?`o6p>oEC^*Kb^hb1D1pm_L zptlwc-X=WU@>#$#{bN*DWg%RP3?!~@y^BzyLCKdW@1N~^FG&p~V(rbGFyvsSJv(&s6dNRB}UQ5oOI`r->9a`X3g0Y(#?pnI^U zp?%8$yYK|3KMKOnRBlO0wAY?@W-+xntdudT0A zrS$VoOb14;OT|Bckyr2cG@^qzwZ^2jYTPG(m^a7e|L9V;-0Em%Fw)zP1$ z_jTV0W88lLDk2qZa`wYr!{*46F5Lz7SNJm&k6S~4##L|j?gEg@D{BZf%0+C3e=9QE z`Pcur595K0Oj@Q1qmbhVGU5zx*2nWJWKE=!0d& zyN;!Cru#_9&)a~VEeXy!Pf9zs zujh-=#6zCEY7rg1@>920kbU+WMpNpf+I~Qg7AC-8G~|0vK{b70)DN@a7MO9fzBd-* zhTlcQ7DsGBCMOK^@nI;4@IGTP4*vsIRLa|NHjdI8e?V#N-e z?jNvzWh}n^CyYh6;o@}sPYhT4NAS?u|AP{<50&6llS0}K2^D?GZM}aXVCgcRNgRgM zsbEk64c#w*KK2(fVh{t{4@q43Gy82nVOZrH_Wav@!H5El`x~>q`V%1R`46zEw`^mZ z!VYpE2p6>e0yg^{KuhHm9ykuR0}t4PKuor^2XL~)7qm?IhL0Qt22wtcbJ`evcoY~| z{tGZra?D91#AlE)TS&cr1v23dwx-~I)LWas&r`M7787y~n4sK$G2nNh> z=P!`fk-r(DE6=dje=t2A0Men~xFRY{Ri#{OAH-nK{s(i>#z7H!>0c~%*+Hz1Gs&u;eTNfF`vfg0f+05TcP;64r~izkG#wX6r2G@- z7^>Jx5uN%6L{s^Ob27a4`)tnEar!y~V}9`myY3!ucL!o;@HMO!pB0ct(A^c4~_mv_~Xt56~eg1C%@OV{FaY|D!5`A3Ah+3-1 zuqdJnnP#B;sq|_MuV1No<7}i0ohji4>J8WkoqqI)==4P=PD1x+(x;?;RlI&xq~afa zfKW`qYTkx3I(m^&mNM*}$codR4FI*vQBCnIx}>T%mwq~`ElyN5J;mF?R)xyenQdq( z2p23x80V5xfR_i4DSoXjdiUcG zN-_EeQNt`ieB%im!_995Px96*uVeR##R&CQ3a4iwJ4IR0jcRyX*lsx6aD_pI0N`=B z&JgYCZ=hS99O0!xV+*G<23>v#RS)`Scva3=eJ=UCHi~;eQCn;e6A>P^sf7ZOSL8!j z{>ku0=u#jIHy4P)>l(s}!xI8cs zUb8q&RSK_3PuET#d2s$Zf<}3w8)ry28Y!}9uBl5D)_GlqEqC=MEh}QI37U(DEpxHFL*xS5S zg{A>(8(q809}GwVq(QW+7~1B68r~qaE^$a6o`a!7yJ7JJvc0Ke*L?aY+pE)laqOB+ z!#+he;xfOtYN;)0%SI4P8qi@J{-C%)ABsZm2Vs}ID+jDJtZv%ODtn`da&y5;<{&no zR_EGhu3N8?h2rHlwr_Y&qgDsk!$L2}L%DpY)f)6tJ#SZ=Q6E6G1<4&K!kY)3r}XO8 z^`TcQ#3V*d37LvK70@#^fkKqw5pClVKnNfhgEWsK>XeVAser|bW*~K4Z&kOcQrEd) z55TrU5ubcawGiA>nW7DtG*@ui84Z-8&1(jeepD@yi*^m2NJs5tBy$u}>jIElDcTy1 zye-mf+XV&SzpEOdoXPtIqE!|*N*gE4nH&i5)CPkpeLAtRw?(~*jh28u(4cP{gE-ae z$dIoAUbVJX6)Rx8@T;v?NMBgfH%jxL#_C?H19ocKOjDttD3#D|W659uHBO)G1Q4$) z1c+*>t()M}sFt8YD%acDB}MQEOKbsUL9ZwUGx|9zR1&r2M>#tp9IzDD!K+0;E2D#V zJQdNxY>ixIOJ(OElhqBfid+j#BQkevumhU}v1!GnCN;}>Gx z*jBF}Szj)C1Tmnz*TEfE`=P_j_JX!*`^gRqx?NnhKq`L+lfte;@nSzXzqkeZRS=39 z*3et4k&xRTm6xZY5}S$2Ee4FF)C)DlAgQhBXj{D%6&sU3om=>F_v79oR7402M$eR$ zvbdeAZxcY)&#&!3fRT+rfX<~(0wCe1@0-9%&BknhGF$v!3SI2NttKrN9FG%{!<@X0 z``}l?7=JL}u+#&H0ZmzjagQ|S9gUClm0K4Jf>GQCkJIoV;C#G^Nc#sthjD_bwgsTO zIyc4A6|dYEavu%%Ay$Vl+LvgpbjQ6BbW`T3OB5`CO9jaHz|jo03HIvR(%~ymAD^%zM1@QnHornS{`7HsXzq>admBE~QGt%;QuLa#!x73zMOS0l_ zejCP1x~5T_`8I40jd5`+D6$!GCzo*w&mv&l_;M7P#>oPH7kP z1z;mj)g(Wag5AL{Fh?7kqxf{Ro85=0j`XPls?So(M*kL8C6!U$9B)8#5ADE|$064p z*m6E->&2ZixF^^yI7tW&eXi{$okIuWU;>#PMRa95U;`1HdbF(*Q0dV6w5J{5D((za zW|aaBS9VsY990Tb-r31plb$ODb1P}D-38L_=$s5z1+?+_M(#jDXyOM*?So)4(w*Sc{sa z8u8_Tq2&wUk=+s1a)=K(RV z$1pN@p}HQqNv(*d4?n2219S{=~&Y7IEF(Scnv+=Ej`_bL-Nm( zo*3?h1cv*srxQFp4;1(vywI&Z##86~x1NQCztRhFK^O31NyQqYygoqmFL+mxmY)gQ zm7dMfJO?AC_#9ECZJg&ZuLJ0^rXM^O-Cu)YUep_sZ|XTydM_Z2p5B2`1nIpKj+nox{ZcJ_AF>4fe3d2xZT;?qe~ zbEkXyAv|zr7l`_%eopdYyO_&q964Pz_m%LwP`uriB#WJ(i0`5D{0{k|G}COMkUzbO#Ie=Mvg}jE$hzQ#OZ_! z&&H>&L%mfRxlSU64iGYJZ6Raqgmy7GAtarfcFl zov=6L9%m|DFyZXeFpXl)9?YD~B;8$Pz}$aEXI(V}oTjx_Os*ZqB%~J8A+@fj!^Kfb zY}UELJtZ+`P%R~42xjd$2JC%xI`)5+$bsN+4vjqn|5Jh6GtWcdw!#1wM>RyB;)7x} z1a2pv2jn#DY4G{YdETykK;IJ^+=Kr@O>E+yOcXkYuIXhansUCkg~>;8;#^In*n(%& zYgd~ZB_;lFI-e`@s6kbZC%B``O$9|CiioFlyE5||4b{6jAO zy@(Z(1m;3#VT!Gnc+a&3i-M>_q*8Of;U851Vf8@7ID`aF5~2`}q8sI0ik)Z#>;k&< zQU}&>puq_^#ZG*ZaNI|Hg_FPQH3T9!f=bZ2cXNRTP_xj>k=3UnvDj+Bt=88 zM8!kb4S>$YGfp*i3H6Jv_O_>;_u#}4bx88djHU)-A%3XE2Zaa3tdL9VxcjV&N8|Yu zZi4%v^0!W7#LYtaTWVH?C2=b(wSU5LgFdWtVEtizNb3)m$papEZe9(H2apvE5zzn< zse~fwJ~;GNF#01hP(%ibXrb`S35GwagZuud<{W=i$1nX+sq_@pm-PIJs6HO`i^t`U ziVYssmKqExJ)vw7{+J9Hqxl@HD?A6t?SCou?}?YOenqM4e7o(8SR${7mY&1bFA|sZ z!=+DL1;TOxk{U6SJHP~>0Ux3M={4s<&8e5#He3M)L-b5OB z@F+$?kjGtMz^gkR+*f*|w`C2nIC-(K-cXXH9)&LiaJ#uO=#=YtPSz2&OfculRTbbO z`shM$yBVEf@MYhO$jXsxf%;|Vd#h%f8Xb^P^rN#cQeh<(9~%KprKkD)dYLBMI7iZa z_eIdq5H8?O4w9g8-DO?t1x|NdLM07C65t>(v2N>dGH#Jg7X2u9smR zWlhz&75+4iE-u45U#J2M{d5_mYf2xyaw-~ z^QGNjj=rdy)Xa3h+zIOYhr*L`8#zpahhpTtgR$kU8=5vK9Q1#o0`9p2+_bDJ#w#6; z@$MQ1``B%~q*a3>aE|j)(<|Xh-FZ6zR}5r($j*ti7{3@lDE(C!5R4u=FS9q;)iT=Bj!R9+QFPA+gVGpc_vvurq3ZyRT2Hs(g2ZB5RLV9Ix~u1XkXX#x zQ7KCWOyULuOz%4d7vB0r{> z^Tnh>R4MbZ0;%~;0IB{UFv+C5Fb?ap%0SZQFhJ59rC;%x=4?_@(!$#C9Q{xq@`{Z ziXi!8nuqbPSaTXX0AEK#TMYvWl%Bn9bdpXkjE3vVlaM|0*)V9QL-&IRj2~N`plv6k z;h5VD;E$Gh!^Li`5vAOn2YoJ@u=|8KVO!|X!E2`E05)JTreor~ zq9~8bKKS%@fco|*Z^V}P;T!^I+lN7$o9}>HQD|7u4QR+MLlisZAv?RM0vtGyyAxxV zJW$ElVOmmvbuDLQpvJ!OQ5oBIF#uyz;4WzAe`;f&7VybhYT{WwRYjP)D^~< z_)O@p49*TC*hJ<8pwq?xI#%pCaa+ihm%Mxg)>0(_i}Ujl%A(DprQ@Mn7C(e(Sxq(4 z?nk=j3Tz;#(@~Au*D1&*p4m62gWnA8|x-9dtD% z4EhX2EKRNB>yd02j0033kM)KNQ(myMA2s}vx6^wNdpOSbQIz&o(}l-^ki3{Cu8WaD z)uc*~yn7Fplko_GW1Oq8Q`mJ)NeeTsv+DeD?W5ygWhc|l`*@5AKy1~C8jOM>7cK)& zsWgM8_rpE-Z8e@|A20#9acPYS)B*on_d|@%oW#sG1(NL68hN&SN`GM)47nK-D;XBq zaMJ+jVQ#o?%E^Y+=f*UC6k2~Z$s09{sVO;QG&ujfH(|&&84U_5Gz^~%ZR!SPL?7<{ zh}Oz*18e0|KGTm`>s&W7n1pn&mc%gPI+*E?OVBZ{%gfp?VC#@`NtG>>@5g!|r{?rk2hWi{Fal7|E!-?qN*#=`^0s zWrIPU3{7J18EjULfU;!>{|- zf)m5zpz+mv1xk%L+ofD(+(lO>%O8)!h;?Q{h%0Z7Q>wYx0B?Q-aZTk}FWT?PqnjU> zHH0dr7_gSoAQEPaOL;crS5JrY5q%T@Tt5rrXaHOc7DR2;f%Dc!!E9S6!B<%^lM!EW z^AN(eIPEQ_JI7;X(EBy%rP;|fy7dXrEuFB&1``0$(OH#^9^~8iMYM7Pla)tr{=`W~ zSJ&)|>6(d9C0*uJHhNf}ydIxuM&D6s^9Zfk0$ccnNg({~b1NG-s!uL^OvVDIe&r37 z#9i?hb!_2~ElA*j8E9o9w% zrz5Tg-Sh;k-ZN)F3N}9tMHUtXA&P9-3@EbjA~43W8SuiCq`&u~5iV!rq&5rT>1=h1 zIgg;8;Fo35lc!+Sfv08Ft=!O@%3_R*qfA4q`fAh9g#D=VJXlx%&BN}@jMBd1O+93` z8te-al!$8z`Qs7}z&$Tc{Cr@h*(Ybc0J!u%X)ZQl`Ee5^gw>1ZU1->!Fb(U@!bUaf zFI?fCa|1Swz6%j1OVQuiB}5Y!0@x2`0k^vrdb{K}e@1FH-;NzT8&;{>`YwG5Xm0%? z>}aKpdb8$$DK?BpG3eaKyp6MMcQY{;jh+MIoxc>$&+~IYGQ)#)Y4{?;ezs_} z6S|~pgPVDXRZYzZS!(o&E$C z(`t!fF1N-=fW*U-h16~yn%}$lq|F2B%I?N_STbyFT%mi}n}?O`+2m=`Deks@bD#(= zl3YhqusJSO`QwsBdDW8uW7QIG*W^I~^9v%NfnNdSfBfsqO9e~Y=`GbUIn&h z&Igm6cnQ~G4lVIk&$6=~ZLSaG1QuW*Rgj;tU%^08ea&|hx9hPCdk@dWg;%;hh4woJ z!WOyuDKMTTJuatkaZPtjqr)h(o;G4yuX&r7Rx*>4bDmOr3EFtpLDSXG15zbEb6-UuM8wevEjmW^ z5zn zg8C9~Vle_GYPah5=1HfJ(~7SbLldhg8B|0~imrh2W}~1-#dr&bjO>7!b>V4$Tw;K6 z32DY9q=~Zqd^vjD5-eiVDg%)1RYH{<7o&PPc^|h>@6g|FPhEFWNwlgm0WGS_vWe{pWGjCkNp>YX^ z;;I$lO!M16<>gAxqZgyj?uBnK#?~QFbMFTb(EeLEIF~wNa{!9OBR(JS z29d_U{AEvDZD+l-3DQ0HJq*K>+0sv)eUnGu$of)2%fw2M;js+}X1UmBNWfGVPd{4; zBN4GmR7BnbF8gQw$)-x$b(o5C=#huC{0O+?{rAD;slcNXK2`C%h~9V;4SRnI)G;cG zY;j6wbNV(9On1&Irn17`he_YXd5qyQchE?G(sgs>H?+ca8R$h*}N$yBUkWxxc z`>{H{xJl8syg^SQFnt>yj!GDJGJO~X98rxM7k$u}s&8_H5FIN++)Et>+_)MIt&cP! z?2$=lYgB%&jbW{~(YpWUN`~bk_t2~J-o}VZuR^?P(nlMCHs+fSF8WL#Uh%mW%2q2& zA|b)Sgaii>^it8I_&ZDswve~*)>)a^WOfP+DpA0ML;({L1x!d35Q8hP+Pg-~CLo&j zlmKWglg3Fbhwdn;tDAGzIAn4<<8${Wc*Z)Csk}}nLX)0CLI*daYuOfAhh;7J1j_m!h4{4&Aa2`u*2!9B*+^F$ol= z=G&P!ya(kl?i;<}TJFt}#?MC#p7jPvD?dAFAGsd6VLjFa7p4uJyG^i^eBCUaoLbqd zl}NB8Aup5S{O&X0aPS7edwhFxZWkSZxTL!Gc$Ax;x!oHnbZf|^u8U~e`#@NaFHSma zT%RVK`hh{itz9fFPOG~w_E%qHx=Kos*RlQGA<(!qtE$>(t(HWKAbNbX-ko*PAl z*e`&MM>l$#&)9^FyRwfVDJ+TID&D)o=EWlxZ-okfLec^gI(Xqv$fF*Vu^qNmyAJ_m z>NeCZUt$Sb5{-+QM!GQSRpDA#s3kDstsq8X_3DY)G^P4LAIV8I;YSqmkwG zSs2%?)lI_Ek8vop7iPwhU1pw;={>XP8k=)wYeDn0fU`Y1K#L>^tS6_+6r5&<LPkPCBl- zh*wPKLv5>-Z`qBrGqLmn zVd%a8DO|WI?;&{j7PDeyLa7sb)tC~37Do3cBzZU?!Ds67EjaZS)*LCJd~t75C~Xl< zC(h;OU!Cn}s?N!-KY$&s=3ZHDV{?TtT`0}L8}tP=eA-ismTecAq z$$ZRn>E~K1iwU#lQ_t@VcR6RJZJ%TRYr7vS{QGkNrera)&z(UK)l{cA3s|=pS*%V= ze#X+~d@oCD;+!N!Nps4ErOAEq_FqE#-|z!GE{BCo%f19$Z_ISNM+L*NiAzzx2;f z-Wqn#FO1-(Iz5{Dwc(QLKLe*ne{wp1$c@)$N9TWonLhbV)?7h5Ri(Iys_cR}sD!Ei zFBOi5c;)Xv#fvPz3gHJTZYOul&i@wkKJ;saL&=r0gWnq3)&EV}aX515ZqRF1AJA*| zZaH`YQ^&izVcisz1((U~^MphQ6B3zC$VC7uK8WLn2=tuNUdLEUDlw|VMxA41340|> z2EXL*CNzJ7&eZK2TtrGG>()Ai#qmDgG@-+x<%QSyM=FW=#=UR}mEIWt7suvM90`vT zs%I!h9>h?*R|HJnk5d{DlML%TPyV1?3Kc2j>b{)Qy5u{9=-!86soL^W9V!gv|I}5r z>%T{HmAG@t5pSM)#Ry?S4@O(N$=kFtAe4QkejTke^DAd++K*t!<$v({D~^+e5f8=H zw_-YdKSC8s6h1yKa^v1dsvPg%Z&1APuaouRJk4X6=kcF_k>8T4&J8Sx#6AC_dD{bE zUgcre{zGulv0{b@$_*H`-$@OyghbyG`tFS%)C9*C(2fHD;<~@RUF+D66vC;y-*LL~ zXPgw`jIIHlam3rSiR+Z2(IG5xZLQS*1sK2f&yxt5IZ$KqRUR`y6Sr6bhzSWGVqC?} zuk*O-krEZquvMpYuycfl@U^i4zoPZ!N3qAHaz32xa2U#0z<%b2|Dd5QLy-56!M}k& zM_dP=am;Us-y^3ym-ZeJ3WRkWx4dwhlQu?&mWG7f8p1NvW%ynPG2f~G0x&UIol=e5 zWT~ONmm79YvZ6`&Dc2lZod*X&_Z^Zu4ahRjrN@p5q}fhL>F^j99>%~?sE>0@anUL@`Oc$;YuOPm;;FH}MAq*DQ}PidBYzk7qzi!E0IBUsv?3J_EFheN&aIXeOd+wMamk#d_{ zXH2;iQi-zgPat)Q$I7P*yr@;hi**UUhjIJ-$(!N+A5&e}nCQ6Bn>+m#S!QF`NmD^G znmQUs1B?HM4Z3TlRfC@PSYbDwLF9C4HB8t4F9Tt}Oaq|{n;~Db&nf8g#9w@b9>SLU zBTlc?d3;rmfZ5s&OmlVsHkS*XaF|cWo)h{TTl%B_0prX`9b7ocYz5q1gkEY1BqSQm zMCNNnTmHs^ltsSm5n$t?Dhyd~E2nv>ak$gv!6bQO>=16!W*tEky6yA06Q*2d9B}5* zSPP>Z%C)N5N;EligVD)-2Ev6?B{-LH*mYA?3$Cfps_mvzaFynXEa3Faf56@5>^{$n z5@|HxR;y{VQkTeg*j#(y32}X$bI7Lu1G;2)!(FPwXISB;QtiFGV3t9mT|E zE3Hq3Rr#56EYy}g!@6z3QB1Hj)2c%oj$&O$t66b3&O*2~kb@-+K8A+J3Jd~VmBVh=|HnlO#nSkWB&E_;WPML4{$#0bKI$7z->3VoL!SK4IoUPP2z*z zNVwdb+A%Kt4^Vh&XebJrm@;PR9Ak^jCTQ<}z~JDzknyv!tf=kIg>!mbFsKRLGWR%U z=$kDw*xwp0+fP(g{U4+au?A($D)(Tqx2Dao)@5>5?1_0OYf)kP?}x(Kb&1fi%vSQD7*X6d+3MOHyRtqSl#4@v`?`cQ!>K;0~eez!lY)})lc zOEpCa)Gf6|Uoo?7kqE!4EShRws}z?vvq~5>P zSjdn0R=ikDo$`I~z!}%6Kacf_+SHJ(nooW6we(dsTXI(e)2sOFAs&PIZ`!y)Sv2ti z=s;+kf*ji$E(|aY^Wo0YtYz5h=lYm%N%I*=tNAqbd~eehsFs*wv$RzTs+g8?{XfpC zRWbgYBnLhZz>X4hA13fuZlx0Qe$zm&-?Hre;W?}v> z4ge(t~^Wf{Lk@Y1#B(VEw$x_rp*zR+q0}xRUiVF zg7RoZ16h1i9egb>*-|xuolVT#l^?t*%m-B_HFVb=c+v|wDaG^}YeuCgakC^n`AX06(1IW!+lMxNRF*SfD+n3ZTzgR_LkMQT4hu0JAqVJ85&|m~-9sVTIsibro9S_o~LS;I3&4PRW4TUYM9V z3IJAK7cZ3^Ge+8;q@e-WTr*4*lzZQF7q!I-{>`c{!qB(Jp805|er!|GZLq&-p% zq6xW1-^)v~)QnvD)pXf|f6BsikJ zuu=p-GcuLtH}m=F`6ApUJ`>7|N*chOV?(O9o3b$Foq1?UXvT-+9A;>VV1lOF!TO=T z(H0M#3NF#wBC1qN1u`Fs>g0=7T`(Tq#sdMl{OM+j7f01(3+-~%Fs>I zhE7SP-ZDhRsu}=W8-p3{2wIV1fEP@>g0xI&9E4A$d^mo1G1vH`-*Iar^NSAFWc;72u`xo|B)KDE%b%I zjj(;TlkI5$G;LGOrYtXp%_a31Lz?XpxvK0<-`m`1!-U$Llv~bP%p*P^ei56LFz>@z1)iF#CHFvu6U2;nnYzIhUxhPnxEYoBOXnp zkBB#wxUlpAC_I|o9w^UkXw{~6?X4OPor_0z)m_iAZ47I*>ySQFOs8a=b1Ed_pI!_h zIH`i6kVD2bmJf2fDxKZiA@s%tlqa;+VpBqghQnCi*THIOJJ)woFG|fjT1{Ya$t6Ut zl&<0dE!$zTbGqy*(4>Hc&tI?4qR1=5$AQ4 z5u3Xi-)(QblJQj1m@Y( z5j@hrGtBDkAviWF1@xxwNzv0$!wPm71Wwge%n4!4qwGRhWWsM zG=Oc!QBibLGuAmhO)`*ks%~<&jWK1C?mBAU*Kj;G#okSTS3Ms#9iSG`}x&N}U9NQ}9piS=qQzU8HvdHy?a9@b&8H zg0IHz%Sftznv3<)we77CDt)h2S3mn(1@`)->A($G3gC{R1BZhe-iVAPpu#0Yffup3 zUhm8fRtH+t2dcNpIRaZdSJ-r#zS?NPVT?dOUOmuS}_lKn_H z4r57HCO(K1*Vs!OebxTNuZh`|4LS0;of{ zzxOm)4qbpT!Px~x#^WsqE=}$O^|8Je7I)A2R;-B&>qX6OqVhOy?QKCPUWe%2ofm4- z03ohDr8sbtGvTJ2i$ys10M{dK+a3sZaiyZ>LIdwPlDUcxX0f={a2e?G>6rk1-9-ir zTj<@PR-*_}2Dqm1kUor^tFV1lzX*W+NpjNK)9!wNw#U^_?p4l08^u&O#i&W|Uw}VZ>GXET zp-33x(1dLRCTts!E9&^kC~7*)2@_A91%Q4$52N!gV5`uRhSQV>8jt`Ty>lU~psuV^ z6&9*pCRk`{9OHBrjGDU-I4Td)p|imy$|!kqpe(Mbd8nd%PtpCnIQ=Zr-2MPOb_IGe z+n1Q^qw+n2TyM9C5(D6*#U)6m_rjEnP7|nH}FQ`#2uQjV zKB&d{aZh1CUQYb}e4t%%%7d4JQ<&Y{)Lt}oFP?_B@YSbRe#pqD$1XrnQW@MoU4^08 zu4rz{Uv(10#I`vr1IOhh7h0XP$NT8H5T~!NvBHJ6AYarRxGDZpaoFt_Vry)A4Ts9y zG)FMRnHWWM*F|70Q(Xba@K*!7EUV3JIeux-iT%OT;oLg!Vyh=Tc&*jFh65(L;tC{K ze0?#7OW6R(^FTF`8;~>MXe&Bd4XADD2QFbYzZQJ-_H`h#i^UT~c#~5sa6`P?T-*n} z=z8}9!Duk9b1QBdXz}IvZ9XehH=v`b+O!Lpb8Q^RMiK2)2eQ>J)lu8Ju!>dIDdQ&Y ziW{R7)l!6m0+M)&XRY+&&bt(+X?P8&re5Ja!@yBpud`~~Uc_`_+@xo7`ULH{6n^TP zH-V42k6XAl8Vc&Phlztg0p&f+z9lVm>R2!?!K=gB9fJT|m%-k~v~4J8z;hJYygO$f zx|^#D-+39d%f4GH9bLcgO*Jn^(|Qw817X=fs|g;5PFcliBjb(lB3gR665dwyzw4PF zfckUYQJ-{Wz?NqBHDTO<6aoMZ3{^~^3RGF%3#zN%V1Rc0jaDg9`PKoIwc~ z3@{VK6`ta6^uPxOGp(M_$kHd}qlbVX%C9iaF>6NW>Jun~P(-it27!R?skF9K%=Tus zls{*mRCQ5)|4fBe`d?{CAHEMGZM_TZVsk?rQzFS#9onAYKJXn%sB0I*bm|SopN#iP zCYIf7vke7g+anhYUcAwrp|aKSqMvG4G-9|O9yz~wgpLM80C-K*E%*fB<^=#EY(`HWSGvDRhNeQ5udF4GPo!M+#JhKE<&l&=>9%G?b zlbVE{axxGNsB#1cSS6WVI*X1^faEW}AHwLmTEtop-jCtO4#ocz!*wrnvLxO>!Z~j- z^&AHAI(@W&Lc`0P3< ztQI+GKvIL-t=hRJRBB$;pvj{_);_lbn4P23AVqk@3!eYsc0;b=4>+|;l-}XW!qe|S z@B9a?(;Sc`^a*R=PV_E)5^>E=hS-$zfOD}3X{u5IyYIw6!ybZkWj|o$*hfPGs5)f5 z=PnR1drU^05Z30J%PVumVg>84fNic*rU#K)^txC1|2vIEYs_9_U=4OuTkBmTOtksOhcOvMFsH6On^A%0 zL}gEPG@N@k+A0}D#eqEP^)MrNqE)Sl%^Gwa*@sK$<%zAIU)xt19IZcwv z*F;E4&iR5S5fQMd1HbGtkmH$&P66Uqj`&dd#WJ(Z?vv7%8P#XOmyAO9U6azf$MhNQ z-=j=--{Vj=OmEsb(J6n(aaZ@|pB-&vM^)i?e6rQb?qWExmY^>uTa76^9htd39smR{ zj=@a4T^W3h^lIK zYXlqbjwyg<@C>V({lteEDB~du(MyD%*dpul5CC8P80c+}2s##ur+sEZUw`!w##17Y z|Co&2)aFAvyQDlpzMaTtx+BIw2zopz6v(j$d>E)hW1x}N&w@A?usEm5LEmXW1Fywa z%)}{lx*m!$1dug&3@cmlFeaQdT_!Bi0k{f89I;Dvd^}(*ljl;53Xt(nWtCiWB(}|gUc@N41by@Y<1`2-42Yt`|!AEdBK6n-c zwb4v?*C}f^#wS~RHr(=2;7e(S%jOuKzhI_==VM4ERp-NBK8khUG7Ed_g)^-hur~*c z2Pwkyz(T{vqq7<&bGH2Mm<_cN`B=E=1S~BTD*k+?)vBEfXT0Mc9UL~%q!)PS zig?lIbFCbkQpScne*sDqkM} z8~3F7A>f{qVZW%1gCEBr>uNp&hCQvSBgp2MEfsbQ;5p@tm%;LxQy|RmEEJ08*y=N~ zu+Zo!Kx)g!G5m9^8&Wa`yAIK#Cz#lkPnq)QFlF8ZOnKT=Oqn@frYtczFy+>Xv`uv( z|J$kHwrWqEynR^T#=jS;i3Th{fYRwKYGWJz4-BKN&w)}U(*VeMPs_l??tsuO&!hPp z(|ENky-z#^^0*Ej(+Aws*i=v+9%}E`q_^f<&3oDdg-*)Y_`rk>aota3*ra;}b-=<; z^PYwQ+IPCu40~@4`sr!M9*2kS^lA6u>EMXmXFl9$hmVN8I(FXX)^HiW)i?TlUb-oinew_39y4-k6z*I4c6I$Qb!FqXy;v z2_cESh^~0REDu|}nB9sG<2r1*Z4Pv=dcy3XMc~BUyE3Y$HB8WDZ$dza%!O+(_-$m> zJhWJV$#;yXBdT&XfDF#}+Q(>DO8{v+e5%g;YV(oF`M`^^eJT&IRGca}_{) z|4GE_x_+Bchpu@^7L?Wy^%veotZE2r)4_j0+%uK}5{9tEx$>ktlK{I=HngM0)RRjq z9vF4gHUgz97hp^^aEE0wa2f=0`slNa61x5=h?Bzoj%6}p8t&D0M*lp845k#OZe78R zfvK8yOlf17UU*v5DVt`j#ZiCbS3xJV&CilOg_$Hw+lBxE=RBj4noFT~t-8|=+*X5@ zBs`7k;Sg0cD{n8*ZK+KF37d**4m|^|fg)%_tKPKQwZdtuG%DXiR6d?G^oMblQteHU z=g^NCnYbOJWLBGs$ywOMKn)sBy%@O?>s+?Us#+X0sVI)&xhYNk8*hWDE;oAWH|mSg z803oJS0f{}<~``ejT`|VwazMR5;V6A%MC518F_3(bMxuLH$Y4V2R5!P2<3oWg%xYl zq-TNk+@J9f+pM>+hBd!rYwF zitUK!A-0NDpIIxks2enHO!>}AhUZOi$xXgWAiz-d^@t4|{uVl? z>q@PpEtwcYd{kg*iP+gG5bZM{F;3zopNk-7kU4oyv0zJcio63|F8*D0Ckx~wla39 zJ&M=N$!Ty2Kfqvs`jpzXXk`)rWJFEgj4kkt%}~_9U={m==FZ{Z>aPMXM>kmh89(6+ z@%jx`R&BE<@_Z)92HQt*{oD;qM2Mohdc#rgL@XwleMY-wC6+jek`;1Kj44=0wLraqV#dRljbt+uoC8LwS{#bBHRktG1-_Tj% zFh&E4Jtg$fOOW=VpTi{k-!`k0vq_xl?gX~#erCn8OkcGMq)Xwx#b>yU5Ypk8iZqJ4 zsd&>;z@mmP{M3-go)eYCj)EycPn3YLLq8Rnv*s!{rf2^x@BfD z9ln*>a_nJM9%9roP@vYGVA#3e$~2CQsU$?aQa;DtY_Z-L{VcFQOzF zKHdH?MrAHgIB#Ibn@Kzi8E*?AET#G)%x26Gjcu>MI7c^WZ0NFQ15|Z&=62Srm2d;< zTSC+~f!SCoG_Ft>d6k+Ap;zG;vnIY~qt*@$oC@8>^V zhgJjVgTRK22DJItjNJBizu(IBGAVJ} zs*t|gW&a8R`C_{HOF)!tg)*~+RP9T9E4*S?NV^?lp^|GMz4C><6#~4)bom$dvC2B2 zm1SCa;KI4|Ti@8nnz{mG zP2Fi93z5Gfikq*h3QB3_GKfZpuQ;<0?s0y|B*U;^>8fcIS#I;Za_IVRFgd)byc}m^ zdGDm)Wr(hFx@#~0$I^gT**7InVrsdINQ6zWHm}sfdOCy2mP2ExR4bD5S(* z`3toG>d-@bWt1)TG0Il_`^FpnYBjDpg(ODfZx|BW^U*SKoQCwE0y$ zMf~N|H=$IxA%BTX0g!qGQgh5VcnP487Vp#iz-?x<0Q>d)A@v0xiWk!N_HS_hT}0!5 z)EuJ*#!+_>?fVgn#w<9S#``Jztswt_o5Y1QQvL`WM8^&Lbu%0+7gFv|x*5)pQHb&< z{R^(-7SZ$a2ToRt=y&-8XP$-B>45IjZZZISME)531p0h_Ko5XZ!y;<*vlXZ%+>{m> z3Tie{k)cL(?k`|c6&VVdH~ADtM|EUq!mEZ!pZGu|v74Xk+C7a1+-<==pP z|1m4GxxQ$kcSpw%wwpze&lxXs%yT6ou^<1AaMX2}cE;X};-IsOn8nc8VI9Wz2XTeL zfjtxrS&1;l@HY)8XC5@5*lz?l$4#1IA%LRap{}(G@!PGRX5jHntc77|=+|auU{OAk0B<=^rq@4!;E;M%DM!py>yZ zxCbD-=tIL+TeV7n0v(KumPf(pg@{ItSPeMhZyOLs9uhpXuv^*=5KL5tqY>Df^xfj! zZzDajEjlNsAN-D0aEg?&#iNdQzoQr3kT-*FUQ~)yEKu`s67RM%KIeSNSW<$QjL=U3 zeY6Jrr|`Z(<}7jcNzw`kKn3+` zfG1V&bgX#SLNfHH@CGhJ7hY&1=cGy61j5P4FO{sv!VkQg(UA78mnl66s3|LzdB3GO zL}1(2Ii7fwYZ`KtZv2GMsw<3mGj!j&8PN@UXF15QB3H03J474pcn6 z9@M|*LxYMQzY8^T?3OvBB4?9TP=#5JOXaVCW%%D_0b|lKtm5mARu&BXN z%J%qb)1{w4@!Y%uif1F%`N?N?{Ac)Dw6r@w&e_GH!(hyGQh{yKb~F2kDS~<>X$?QO zYFU^SPnYgM(4^Pr2yC-_>q8y8;-O*Z08xtb1AiuQV2d2sp@^HhZhZ!VM-UCw`K9;n z5Ge36-JU%e2?av#PODb+0BYm1@rg%A>C2s9yL(q=??#6{bHwV{&4`rjHcO0 zlB;pPNQ-OunszWAh>2?Bu}Ia>L__ePqx`K0z$=IPZ=9lLW&2{MC{+1F*fsiQ`yyo_ zx#}1)2ZC}I8Iu#pm>lrNfR)6V7AsTMrU0ABa9>XgCL)z$?*NS}~7gf>r<;U~RJd{8_N zKh5N=hXl|-$UqvBGg5pb(1grst~{QF?xyHWB&3-QUzz2E^l~!sjp=Vz6Oz@?vQvE* z)2bX_i670(DQC!>bcEz#u0RNhZC~a1TA<#bmFw$CW7_)K72vG}g$Zm;vS5A);Y3?s z@j2phqK3Y-kYibFxw7uC&>y-{M;`IPG}f9QTb@?J`~ioPdA^qP_mPZj`uPE^VXIJX zO!RP-fUa&>t{JK&IX*`1lxsE)4@9Qo7h_?l!v3#+we zB2|Va*73FBZ!|XsL#5QW1`Zmos^KfVP|J**5u+-h=68x4 zuT2|#lHZg6it3WI=alA((d)&&re)%F`Hj3lN))E?mS$?V2?nfbXx~|06T-WsQhvlvn%`Nd2gEe30pEO5> zC9qGw;HTaKu4rVG#?;eEIgGGmc86sv35!#RvV*Ff)CBZ%QNFY5`kJ@Y=!yudL`420 zBK{FsRRp&6lV~oBj|gi-Bp4Zygpky~n(`4C(0|wUHO-5Nt{jvZ2l*l>7B%3Ir*Bqf zM28dokywbv)B`wrbrD%zL{=A(L#>FMVd2&{ZAWtv_o&oAh)Q%VDjRuJXciScCwC8# z0TiT2eP8o3jo+x=UNhwelRqjF9o2gX{?<@MWt)h~<`9*gAgW2{kBVlC>b)#8Dmz$I zbX-(6tEgnZMCq&gz9OMOR5V;v&fTL@_aG{a9YvXfdJTNduhr~@Bu7QhnACWbltO<@ z@@Qj%l9+hbF+ojCcK(>W_!AR~#)O??62YfM4SbCRKQS>sV)F3Fpv(k_=)U)BiA>o5 zn#EGKfL22(n?S87Gie(nZG@0tFHv^Yn4mW%rUIgXbd%p#w{B)kYAD5on^OOVD0`&0 zURg}GjhG-cCff&GO+AJ1TTJqh;63T@MXF*VRWYe76BEXZiIm0UF2DTi7mon{Dh?~d z2R%_-$RnmQzSrU#7xKhq|BDL;$Aw(w|CRBCjB(-cxR5a}4rN?9ciqpyyW(2C#bpdH1|dS!Vf%&-Er47b`0+2B2I4#XHrqJ~5*PWr80E;yr536LGnT7#Aza{efb& zT;Q?d2YfS=5BSQy8JBG&E_J%%Voja&aZ_Oo^QdTC7$dG2gT8vlTU_S;`lM!LJnyKO zCn1JdLTH~5+NYin)VOhfTTiK-%!JT3p>=c_UgeSrL`f%~Llb445E_?zAWXKTgltI( zy(J+%PfKL^x13}iA+CPhsO%hn@^vF^0c0kG$_cTN6Qb?i-j!Ron7U9cYeBJu@KfMFS)xZzG{4 zKg*viA`c&JW~SvYL++wp6+Y;`xTP$+^BWu7UVK_{3x3iNKtuu%5oAD`&c+lk zHKur}F~x0Fee+FgbFfw#!X>n(R_%PuDhk|L-xUxW-^lhs^BjKCJb@2pF~$L^tk|Y; zdrC#HQM1u>mt=obzSCS4py!(TYTcl>V0^3hSo;S9fw|IHD#DFYd8!u{X50kZ2aQGi zWV(ynM4eG&g*tY$vM)zRb1SPfnRzONMpmf-Nt8amF|Z{Ii={ru@<2W&BWQ;bCDhCE z*T5(98`2X;o?{i42}Dw-25k+AZFOqIi$j*LW()YV=7X>o@|xruL+bQWAQ(AW3PfZa z-^9zLo0vDqjWv@Q*Em_$Az)nNfFVC>8E|(DZQR~~@pe-;_zl%fyaT)(WM0-@UPrD_ zW$0|XX|A_2{3N6ch?2*RRbgs8_f73u`fB?yJx8)z`r7msDg|Uq$3;7XG1=7djaJCO zw;9I}IbcYN3y}0$OJ4(k7BsoX>6Zbq2;)-+ySfAIzM|1V^wz8HZgVL1q-b>0B|jLI5K4UzqJ|9DgtYC3q0QZ> zkkJJAFymAoY!rxU|qy;&?ZbgQs_JEffkap+z8tbT61dZG0`l4kRiDMoJdkHg}_!oZQPyv1r z92k!t-w4x$O|&U2=6CWG4j+WX=41@t2=hq<06d8aG(RPek!*o91CAJLP_{xx6EVOy z!l5R1fNvD67}{ukb5ekB4AADn7#_}pHS|I|UpuJ9a47crQ>DJUGO(*FTtPVlKuu%p z%J{4m37jZ=RugAJUE{N!68kjf(0#F0%Q=5@mcqUyqhBpP=hCy zY$k|3K1*kw*m&AJvF6RpLDZ%_Hs&9KzI+dY632qR?q%W*1ZDG)r-2cV)})Mx!!6fY z;7*81fmbFpzsrRU)?h{ph@l9kRBgbWIy3@E#6U-7!5%~Iy!8%0y%qB1(4u2V!pn{M zT2s}qub5#Q{1h@VI)r_7J&4_8#C+{(TG&_Q#Xz()>aa?5&6|;0cF#j0iP}`Wkp}_u}-ZFF@6fo3?`!zAE%e)K|p6ZNYETqdo=C z>HNioF<+P_oY0Ni&<8PJQH~;ByNfQq1kXQ5+9FLUjPsYOaV+J{4BZt)R^z^+3>@Oj z)hnry7mCMpk4^3BOZNMFLcWK3ukC6d-=vygHwPM zG6ds#_x9Co7>H-$!j$}n@Dq+5{MY*RcZoP2b*6`V`(kC-p;^fJKmV=zXPDVBRxxSc zDL2>o9Rf?xglXReS%IW|r_L$TzC--aUwT&Bm+HQ1+IMlb)}7xWc9J%6DXxhj@cZbQ zzC>AoO#?>W@m(nj(4@C3MbH6IrL_x{Ur}5=&gg812eQ?8Wp-2os`3NID;Q8+ ze@h#%0ss>g0EjDec9dg&IG1q9)6oV$q}+V#`~uOXfRJ1yE|!L zIw8c5TsgMrAV=fORBBV+z;2I#EyTObT|c@#Hp;*8eEE6YiwFUMZAowZ0p#Iitu9! zV;$_R7&k3W1`BK;!lcm`Me$AE34$0=WImvubSasRI0?|eAU9CWsk;>ugU-nTEi!U)Ir3w8zV#!F2&MU&eAKj2Eok6SHl&>G|& zv~|x&wOX4N*65$xwg__2nmyEL$BFwl9ON7fagY$5RsLGedAzU{{swJb4ph)uAZ1D8 z+PK|y+38Cn@)?VBc<#f<=N30#{*fPS${Y>a9mim2$;77$#p!fD9SMI z215gt@~wl0n{8tA2I4_;D0qe#09yIL@!N)PFKO)mxT?dyeQx#0aVI4WNYbqnKS%Vh zrA=-)6q*4nw_M}XZ}$Z@EJsn*+=aOGOmK74fTDP?C4L00-H;Wu7O@N3zWkb=w&{gH zV*-Ge(`XhP2`W=wRR3dY1VrO@I;Gs{MK#&Aet?{^=g%WD!ixO~6W%c>{`80nf!p_$?7)?ZK8?lvDKq$`v{q zKfbM?MbP#IDhKV3K_43S-9NX#E%>9+Ce=rUdf}GwU<<|vZC$~kpdBRZZ}zRvMfV)m zQmV}(SRv;(Ur4Y`%ti1?jD(0x8i!Z<`sQ#b45c3oki{CrsV5R)@e3Oc70(^CC4iOX zp!LtO`r}fM=Me~V=9%N!gT$wlzeUDT;i&x!k>$DXaEBQa&5$d2OXrrH+>x|BxicW4 z6N5~nuCAat7!cRwbXY5n7*RUX{WOu=W;C|-pN<{UxWXX)jBmFUh3skUNX~?Ga49sB z6D6TRXctHE*!+8t(-s%*KLUb~oKAL@mzBI74RB-g7pP3y`u2!s7K4z1qopQkM@zkJ z$Y4L{?Wh4}1;7FpTo=!^yB^B4TRez9(?=*?*6c7v^g^;fQoR8=OQe%*wwzqsf1+s( z-0nvR4yly^fzXHd$nTup!Fm<;kbt|zw4Tr6coY#n;#QyvefVHopy8+OG`CZc}*uw9;+;xl&2T1fJINbDAo@ zZ~8I+f^%v)Pe`Bw)%|lc8qz9U-yAE~r+*OmB9|J=-vcD7n#@{aA!b~Vyev0@1{Htc zp9S=R-6JCle7?}}et;OE8uX^MSn8icI_f#1Uftwaf3W*@m1_LB#g>BBGJS%!&3y-U zZkfCNbFja*g#A2fxfqQfY?(jt!tDrOObkrcrDVXG^hE7DcH;TFv;DWmZ`Xbz33K_e zW+lzmBxL1tYd|YaWZR;P6 zSgeENCjQ)@s;2>tHYw7+VtFy@edSH{G;-fl&;eu0T-@9eRMBtO6}Rc$_m&Mn;qbth zSC{|VJdTFdVyxG4)`*qJTiC2y#%cGh4W0S_?92GU=_>-6v_@b3(=d|0zS+wBBcy8n zMZTp7*lb43aG{6D;+C1dxkH$3wk>(3AKo(~hXU{-$m;H5hD1F%_wi!}T9f4d#>< zgVarZo_L=>e>fX=i{0WDFbQ2mxL(NZSsLJ$#J*fy99qGbXuRYd=m-LW(|a)j6Nb~eo63~zmy7LBF@|WCM#1(yo5{0=q#UF2TH<{> zX%uj=aU!#ghsF~GyOm7_ZzIMu=&iANkO2kx2*rni@>E#~t@CL=YIa03p*lnRPTKu& z|A*mY$@DFy4`Qb~d|8mgUfkCCJs4(209RIdKTPh@K5Z83J{mb#1cHHBa79puxBv3$ z@;=|%$_3q$cP<%lu;aD`M;%`IM43goB`j*s=}e0r;8GYU8(F_(Lg;8CLsC=$FpUV> z;uuqhqp9vFg-j{FYZza@o`gN*a}s@%%7u-3)4q-|g*`Ivb~hU_!Hzh9R%>l&cSqN{ zvKn&-apV<>+o9e(7&6*})2K+2>cN~CUAjjn?%R>8rx%hIGd%DH zY@!21?UGI@cF!mt=GS7*Yd#z6brht~fE^dwp60V;6C_3oPU@Vb*zy0QhbeLr#JEGY zXn@GvPKY-)fk8WBu?~k@D-I&4?dKF|(2OSa8!ohDB*ET|bTeM2P(4brZif3Wm2TFz zs5a6h5iJ@g0DsCppdfP^azcO8LiZzY55CAO%|g*P2d5wYvNg) z>(L6Dbu)gQ_z7(!LzY@MSsr^1Wb(qEphOA@{9{ zRtA}7nj@27K?zqJK^&BiLLY!uGTGT7dyELr*r{f;ua|D7fKb{7h@KO{`Qx8S1?*{x z?~fdOwn0Mm961D918~17p=6J^&kqj@$!3=xiT6J20)3tmSnj6JD|Y ztvIY-1tg(HO)N0Q=m9q~2Xi1xFn^05EYJkpvKX_i|Fryw6qz0rTIx8Fvm;1~#gb=D z%t|ugK<3~)z;pZ!b5>z*Ja8!vgmM?YVA-zmjb&X@)F~{CUGi^_H<;J4$KZ#a9Rb?p zg)-phnVbTNKnc<%REUPuW6*@W>V38T^YR0brJAj*r<8D%mP$y$mpw*MD^t*RP7W({ zp;6oZ^I$8$?3nNXsIJ*<4E|E4Dj{_Manfo5wP|vj_q9^f{=+0`h^vvT!x#Ppd&T^T z;klSly$XhCCE@*Ru?00BED0?x#;QL*1~wLXH5AKAE8{Eehi4mxtap%urf-$v%nib% z(0qV}wUM}`q8-TTZSUjylhIcsg1FA?@FKNOhfV5t(&u&wB0y^mhnDLO zZNzY9TMy!t0W^*iS}Pste++&d3k1r|m-|KIx9ygc{8P%M<$;KO%`6x4ihEK0)ETw^q>Ek)#ZEB zk>gJUuVW!8!`Ke5|9NSmoC0}gY5y}?{-3WP^U1$e0x&<^Wf~S9P^)rgAr{hk9zp&ni z4ANjgrIPuo^zli}jsS~hhdZYiB6Ww?kL+ODH+&zjH5ZLk*_010@SD%ES^6ywoOT+| z<#ZC4J4gT}1sG-jW+yy2%?ATW10+vM?*o1B2H19x7Dj$yYZ21#3`Vh@1AW&C1JO!3 zQ^1Kk4!l|H+<>mR_{I%i_*_X zw-yb1STq2^pn=rnMk1W_jRKuc&wcxNo>*>voN*MwV)!lj5#)BxwGRC7`G2#zd`|P> zNa_#}VqSP*&37RtM;=H2De}GWPluoT?CP=M7k*>)5UteScy4w1FG~ug!&X33LQT+G zdz+4zNR1RP-C!&X*PrE3rprw~L6mGiENC3!q>*2#RRP)4bC`vGj&z~+zTwNCty+t8 z_Ly7ii`Gwstn*|Zq7`Xo(+N>ENzfE0fp!KWm&%tc1RZbOd6`jkmqi3btKfm z9ksUv-2Mh$@P3kmqx~dt$uu;5@(1>SCQd|OV+S&lfaVr0tvKx=5(A{gJZ=>V$%2-fTUxtm-7#o$FvZ}0p~4E>mfNnKTdq&t zLorYI45&j?F%mRYP(f===lT~SOLEZEB8!1IshP8KbHu~bX`=m#e+;0Y8KW?B`LSsv zh;To=T2xRmXjJz?(^0ye<{+;lW*B>n+zR@>G z_lrNVoFszQYlSPwj#Y6LG@?b*x6Yqhx0{Lkd6Rpm9i)&%{7DXqfDuZ}g>$SS>4iXV zdC)5BTRTXG(e&K=gX?!ZNj4>H9J2;jWH<-@l;c=7#j0W8=L|M|g- z_Z&0S2guj%S?E}o(nFoYaw3KStZXOdJt9A@PB5A|`=g{_9 z9+7yAYH4a|w)}vW1Xhu0lkxMa$(Dr(LrFXX8q2;xd(5EhXclZS>vP6r8Oygqze*v3 zkqO`f^vRSLDs7Z~MGFV=&Gcb5CB-*TjgSTOheS2*prkDUMj5nV7sV07{;d~pd!MQT zaK_Qc!4=fHagWtoFCKnae?hGeZwz473R#dUEzMlnw!;fwb#X_;WJ^+(-)`r=@{Cgo zae@WOVfm+!E>QkFr1`j(;hY>r(2zXHTm)pI*c-+Z4R#cbB(LtM719a%>5s6pnjMCH z>S}t;3*w-tR)8a?R7#N4<84;iF=3FgoH5yD8VtFPDclGVdB*fw*Pw}Ooi*MZ`2!c0 zv?Sw6VFAzyg?xb0WeSkgsIUo%$T7ep%kPY3XzFr>cu>cb8w@Y$47f-GtbF8L!OBP8 z4A1@Y>Y*29iB}GC+60wGoH7AFRiUIxK~8x>K#%Jk$+$ksJgTJLWN~ti#p09{YpNt2 z%PcMa(fjZxPt%#t8<~FQpvtpOuBtL`(FhakBh?%rI2Xq44}YCEX5p7*<;GN=42En(&z= zFOrp$4`OZ05&?ZTbCbNN6-TCMXUNd&CenDZl9_UZq;KWyaJ3XUCz!98AQ{yP5Rm&U zO}^*gemF->uCh)aAgg9VZjku1a)XA?KYVfdthfsFK`MYO&dlWor63V0)&g&GJG}W{ z5L^;I6@AF1BP6GzP42P4UXs>@RuEtnQ z7NcD;(hnZFq`?pv>bSq=gIvlZizb&vOJ;E&d#jVfsq=Q>Q;Q{NwzEtQgRRGVw|CWYAQ_IT66q40dFc|YIZp&6FLcup*UdQ{ z#2qB6aVMiIB)bM!P+9N-kus+#5)6?E35a5@#)}pc635DqP}HCg z6YDKA;644G;9Y15`R-x(+`BFw|7LP0V)&>QIhO?bt~SG;zU$(#b}{p-?r(Zpdd59U-=-S2~FS|RcVD5?mogR^+H6*C^XOMyi8p2 zS$?_6z$`Sp)*KAg^wR*(Qh*n#*D*BGuJXr(I)XXa7kNBQfyR+3M#wnHZE-{z+q~@# z4y38YH0l%h$#eU(N>Ta#2-uldkV-37rmg?H$A+(YAsA1tLV+qtBX=c~FFgaVdkeHq z19sE=zH^04^%aU2)^erG!QrB+P#;#uDr`)UxO@&nJapjg)e(n55907?sWFjaKd~o4 zU-BcM!hWCnzv5w>s;_GO=jF#RnU|&oK=?=+PAm*Yl0FOo??EdKj^B30D{cQ#PGkVZ zOknHuJQ(#@**Xup#FCa39h2`PESpv#?;{B+juV4HB*kDrK@%J0$7Kx5 zV0#W!EccyCur$E=Enn7eRZGfuS%1F}7cqZ~O`{j0>r=FXf2ARYm#=&(>|J)RePh}LZ{JHxn@ho6+0@rpZ+54Yeh#axqKs7oCwiga{t3$M$`BkE7=yA&9B z4gQFNndO0IbOM^1#zP}ssSiiT5%{(2(L{QowTo$c9-xXyGeO7eBj5*`2>5|k3-A0p zv|J7|k;3ZJUiGs1k>TyXy}DfSIJd4p@IUU{T(E=~F2@4cv|pz>0%_@?ZorW&ftzxA-xK);w8w8px zHGsvMXesX#nhtHZdosC&6wZX_Z>eNR2?T9N4$p6_Eb9dyLcRRozNE`mr|jiwOgRO1;sTM$FYo9Yu`}M%{mWgA>ozV z;q~vWagr;cLtAD=szcBqX&b_p85ZJ{_U0$UfTl8OXyujWIEk9)x)0Z1YU6Z2eBRM6 zjqaz4+Y{o1cAcuBgw`%zjNht<8_(@f?_om_M$w_6(lYZINPD-5jrNeeK<>#M?_0#k zhR^-h>fU>0hI3P-L_Hzm-SB#4Qp=zwFq5Gc_)3s!Nw-vI44RUZo_C9uIv5&|WqWQG zwUwxt2oSV(o%wPDaOTzhp=jxLmeZfcIQ=+BMDEP@wP&Oa=)>sr^npp4P?a1s^88}P zc)*E~;J%Tv9?y}6zGMD1x#<+rlwRxwjrGds>V72AWOd-~N<0>AobVKWfjCy=ECy4# zd-)L{_uONyI+_9sXY(Jocx*|qcppuXdYQrc6D&z!8Jcnnp{cCB%8FBfa+|eMx{Ef> zc=S)8QEZ;AiLU6;8^VPL-K?V2chm#@B54dsWwb$sh8#@FpfaBKhBs4{Luw+#m!E-nz`t2R-G-ZxneRN)B zGC=_+8&uWmZAEQq6VR%a)rZcvGJAQbUTmUxyFlRzPhUN5Om?hl|-nUREXP#nq ziQ=G8t~TnF2*4%w2M9|U%#T%hP-s;8P|)a07t0TzG#9 zkH}(SZcL?nuI}@rE-nuS=uA3gJg2g0X#LGw=m2QSROYK@YAW-0>y%tzFd1j($IA4F zrg|QBL=jGCN)3TFr_hgqV~BznpsC#mw6U}Eq!7?y2$ybI%Rmz13r$T>sdfO}4HXDY zwX1dSpP9>PUp!~*h3IV}4LnyVg}t>0Q3Yvi+#R!{9!`UV8R^`t%7#z|M<`(~yPFT`5P57(DPLv@Pg%Vevjf4L3CMr0+b z$pJLgxFhJ`v;0V1E+v|9P6VQ=nMz201s2AyJimJEpV|B)D~6U-`D$dS=DDS>z$?Fl zP8L>Dz`53*WF0Ljsmt15Ft4=5D%IA{wcF@f$c0V@Z}nc1b)==jwAOrp1DAW$w#e=& zQU$Aixjt-WDU`$wp~(k=#vxt)xPDuG07XN+iKbXm)u3%M1srD+2ft@*r$U&c%P=W=Zd?$*qaMZYIQczJ zScheeSj?>^b}DO)n)ZZ&`0dz*W41alIujt{IV*ok+OIyWrJ<%hx0;%E$;wJpN*1e{ z_TkyzyK7gg1YWOYP?|S_!_T<~C^&n+khPAaWuM-++*Kq;e?EASyB||WP`;i>a}_vx z!O2b1nvt8-y>IxwkF73_(!2;j=_`T=O1vT`Uv>&MnYpvdq~f&IRm-O%J^a< z>_L>Ic&Ln%DA~{{r#pd&0~$`QGaJ58eKA+v1e-^i`uq%Ww|o-u2PJQ&&G(W_pa%UF z7&d5myENEI#)PO3bHIYeO?|++njHd5FLW|xDK+o$9RiDNnaqLXx0OrD z`CFP|^wrP61K=I)9&&bAKiW^Uni!4(L7fQNuus4J=V#E7f<7|{g;rV_7nfjpuD3(G zz)Z$3m8d>cz+uz($IsJK-8A%c&YP@#t;KX*Hk>x1`{43f1P}B@E_1vPla>yBA1{@h zYe&l{S(u<_GX97Z>kM*@*}TT*Ibsh;bt4YWvA=~PpmTuSm%0lfOT&cx$6H53IHDn9PH8GlB$60uHU7L}JJPnrm*m)c!ZOO)%N6F%|s zV|{6hsf#^pxo?|Thb97&vjlE*j>xCY+*AfbJm}d!f}M17aDYPgv*27FnJmtgWxa~7 zjX_wXHm0YOJT$Tv&bIuLo!|*g{0>Wtb^vl%kXwpvV0=mF@l-Fj8aq{Em1GHQe$$ja z3aacIUa8X(!bDeU^EO^vb|oivtAs#kh6`aj^D!ogwuxM9(Pk@bwkzoFCnW}|oFQF` zV`xc}t3{cLX-1PO1-}>b7oa@ANy8g;!<#fP)~wD!zVXzd|=K*~XnnG`S5)U+LxZ6aQ?CfHaY<*&lo)7gm|MXgnUy3a$UOnnkuF7IC_KU!u5K@L)JU-5&AQZ0 z_yb{Jd6xKoS=?J$v7Qgxs>P^n$j?s^uI0fXpQ&Fp7L9HLqG7w~lvb5`ek76dIs-VE z-43>c0aD%(+k-~RDgE{lr;=13riY^jU8ycPK(L6TW>RS=@gUXF;#m+d@$4R?Jb9Ls z3Gpl;!jWg^gHbIUa)cF=$P-DzgZ3CJSv*^1*?!-tYujw^b1*3L7Skg7q#XbewES3q z(hjhK!lz_pX!!v#Z+uM{(F-kql{(~>Am}tZ1f5n3>n82VioA~7bTwM*1&55pi3X0C zj5%Dl1_r2YskADW()X#`(Acr9MMJb{K8QBGZ&Ua(r)J(lcCc=&&IQ7q@{iTrue!Sn zd!<*RFj^qBnM=kvG*>+N5MVdy!vuyjV^T>qW4iC;fh_nbZAY$%xZ^51FpPW<-%qw4 zDU9-jtBHYD&_#R1-J7*O{KcnNU-PWjoaO@>J}p<$+5~1$+6JV}aQ(+<1Yx#-^I8G2 z7ZtqYE=ei>>VubyLQ;`L5}5ay{}vj76er0uhAF>-*~sIgu0Z09h=SM!ue(3or4eY( zApeJ0@n}sA1AV&_QbvE zz+$nmyX9ik^lTj-sH@2&)$yHl_~~9SZuQK^+?cI7meH4=qG+VwEk2KhakB50Cl_Ts zPH37B@=Rk#o~6X9Cf5E4-5hc-NVaa!?c^eaG-_l<_h4-`$4`SHX+PC1ZHZ)H(D*{~ zkd6qO1oE+Agv&rvw76`YbgrQh2IyxDO-V}7XeV`gZaTRT>DrS&#%?o>lyf~NyNzrr zK@re0E>Bw<7B-bCNmuTB3JTeSC0MuwaMQ4a=H_5D7)Uk4b7;mmxpiw=iVKBvb zIX5%qT%@m20rM0s(8$4Si#E$Di0b44jfhXby3iJxkS9PJno>2$@zGWC(0FwDK4ev; z7kb5=6V7vr*+5gshBRe`Y@m@4y8Li`7dglLI~yKiCN|ZB*wT7VLFOt4OoI-Z8gD?G zVnv@Ebq_@M7jNnR@nO>C(;DHoZh1!zcX~!Q!?XVfmLqD(&CvN;yxC+$qMLcP_^m}M znNB)dWXmLC0ZvjkulG(9M3ovtv*2j?bUA4H0OT1Y@SFArX#04zA;hibeKIIFKkmuaz^Sdy#)5qjN-G~fLN!u*6>V%=)tcM5wa&`H&9upzg z;kBP-)6;C=S8huR$7HyvDorym0WER|I87Cm(vskZo%UxTXMnfU(*@Cxfe4Kxo?K&G zOYIqcMhbSy$NN;4k~5%QX~{uTp#wLlQ4yIR2AvV7VR58j=P6GPT;Rj0W{?VL8S-HT zzA+yyMM-=wXllI#4PhE{#9T^io^B(&QO}J-U+gN9I2;&kn`$bP zKSqG$ZgCX*-D0y!r1-aScYWqSQR$7t_d2&~H`B$v2_~sixktF+!eFlbyf-530iyGz^&hFey>tA@Cgj zMLtKl;-o-EdCI!!c+s0#T7~9dI>VIzn$5@<(1*eBrw^ZP4=$3GBhQ9eYC%a5isuwy zhh~ig5GPX>TG=hbgWtV+D@;I;P3uX+4ov~iHK#y!gJdUV39{QTiOp#A<;^B)@FpTU zH;pO|ai8mci?0)^g@#Pj`xZvW#01E8eYl!(=8wTKv@!(~=((L42*)#@t0s1m7u8@c zM@^R?`5)CWEeA;aF7v#rDX`Ba+oDLTh-7fXH4W!Ga*qz0)Zs@-fZUZ zw=NaaYQ6h(S4wq;NO4`A8#cr5{=3ypNdavaw!B-ma#3$E7dikS-@&w?49P*n%20Xqi63y*%Oyn#v82 z)+Ki`pwT2US4e)Bdtd4FdTr$+<-ExmPcBE8o-N0F^Fj`<{;SpH@TJeK-g1<9tHhbq zOn7-R5$Hc5@kUy`OawT|>ooa;3v-~lQD~OL>P%B!5XJgx1q#h>8EN%>Ags&*6rIFt zsZQ;9Vb;0i9PgBnly=DQ!y%B_P`UU=%??SGFRV5c$!gW)HU~)3exk{x?{&P%&mOVW z8TbN56`;jA(xItT^W+Z<*o3S}u=iaDSmOQiUwseI6-HJrD;Zc>rht*6Tytm$wb+ts z-Ecen?tfgp$5(eE1Tu2KJA9J`*i8(SaaO;a{MH@j836nlJ6oQ(NB>v3(HK>-ze#w| z;X*KFkb;C@_B_9jMV2R?tR(r0guYPjVfKNH>2RT9Wd?H%o>>jlTWkvAlp+>sI&;gY z@rl1mB;eo*(W#7neS|zStwJT5<;u9(DEAq9>F9+d=;(!HI*3h)-soV0Bl;UH z6TH{>2-^jySId?QS}ik-4yiPHj_(@hdYInsU&gsUWKI1I!=X{Ge69 z!c5Tya;9Js*=-6YRZAxs4kUADppxelHio9|FgKuN%{;C3pNt9ENME_pO?SNnR0|kn&j#Yr!Eb0vi&pKgokKfs&ZkM z@2mCaK|52}VREJrtmY4FL#-geRSl3N>^!n$`+uu+8sG3fk#(kz)Xg=Y`bAWTcZSRX z?Mx*d=Hep1#1eQ)`qUkFcx}MyPxD#WYm~0El;ZZ|9~m+z%poRm0;5SgD% z*8zpe>plf7F0inCCz2&m7){vB=MAyF6h+Jj0}_KOcL6o5_*T`fB#>$(z`{1Jz0NPbn603SzA(~(*M*_CzqW~!5d?wNM?TbS| z605cvlrF^%Nea@6*pp<$KlDqh$KQcjnxn_|8s7YBZm#f&$1D(ElZ0fN3h#iB)t3jF z??WcYc|ME_`CJ+NIZkw`yvs`G&jld1BBe$;zw8V=jowcw`Aj~4r*=!!kk2noQ|bKr zFrl85nINK4l7J6#11dtDDPWXa8yO%7o%(xSaNh1W`7#Y)*k`! z(d??bXRs4_MzTlfjwm&Xj)((FA_;x81w+!>KvDpEKq85QB*rJTTmrZ5?$&Bi{h1B= zz}zr#a(3J%na7`ioOuBynCc0KOOQFJ_q0tPJ*P~SpD*i~u01lJKtiNIN#7EvgH^QO z4}ba)_dH8Z?)altgM2U1?uZg%%J{HgW%{>c`u*~VGj^>m1>WK@uwd~RmXLMK2EBM|d*W@^{Z}oXhMJ*(0 zs;Bi?oM}8QNjO+aBA8&j2aOifqM;A+Ok>XX0;b*ZNV~1!fp@YPK201kZ5m9NGKEY+ z9|-J9|5p8CfuBhKZVS0w3pud?C~eA?Se(6k?Q4Vr5I+-erpgLZ?hx{-b4<3}g2 zg`|1~sF1Z+!x#P=cD;CF#b^0*A@_E9uoF|Q5?hFY;W*uOul#2&UQws6d3t^m9w79e@A!_Xiyd>weDtC)?ds@TU)IR$@1fb z$DC=Z>A-gS!9bgOhL5Ki8hJT-4rD?!6%+GYRl~IWiR4x|OC|cya3S&|04FhHo};EA zpK~M-0I2SI;ruPEK!{^AjLC?Arb+;4v?|a?q8_5E4~o0;Ij)SFj^`QRDu1w7ruAc> zC(VQc-*NBGDuw67T+rb4{4mL*Ip`y)cL}YKs-om1X!JSA7t+N@TS8fXoOi+qk_(mT zQAvMo{b%N)Xwe^0;y!sZYDfxA#}nG3Xz_$ELL--Jn#c*@f{42|5nBWUnxanxbV-fQ z=dhRN7oLAhOi9~BxJSQz9*wRCDNxm5Fo1IJa8A-!s!XB%@W3||hLk&lEt1ACSsi4G z#C&ji(xO1sjz;EuE~v#%M}zz$K!^6e+5W1eq*M#glwZZMAPpx6L)wZaf>PFV1lv1) zhp`C1nvW~pJa>V@ob>bf?H$)h0ZQe8B%w7`6cFFlbrvH*RBRfWIu}C|tX;I!Z_t3R zVzn^8MkzA<*srhNeKfa{cZaN&KCAGn`(ys*IGwJ)wNUq}~=R{(Y| zzTZB-s8AXuHm5{7k3`#q>qzi-H{XlD6$YxY>28WQ3-&j72L@Dsl! zxhPd6fpGI8F>38CfN*yrYcQ~}1_PRzA_##o<74kW3vLYEblG^}VK;o?H&>SrtcRcc zMs7JKIY?i{$61c}GcUjJLqGE3%lDpq@`>kv=%-$M1j~K@#B(pb^qKlMmgSM>?mh8s zKl<^P>OV0w+kZa&$VVP|@+o3B_qR`xrVq;lq<-irQtH-E!AZLh)g>AqxcB5I|Iue2 z`RKhTo_^^EKKO$#{=g?b{o|i}