Merge lp:~phill-ridout/openlp/pathlib2 into lp:openlp

Proposed by Phill
Status: Merged
Merged at revision: 2758
Proposed branch: lp:~phill-ridout/openlp/pathlib2
Merge into: lp:openlp
Diff against target: 1632 lines (+684/-251)
22 files modified
openlp/core/common/path.py (+61/-0)
openlp/core/lib/__init__.py (+35/-1)
openlp/core/lib/filedialog.py (+0/-58)
openlp/core/lib/mediamanageritem.py (+11/-8)
openlp/core/ui/advancedtab.py (+7/-6)
openlp/core/ui/generaltab.py (+7/-4)
openlp/core/ui/lib/filedialog.py (+113/-0)
openlp/core/ui/lib/pathedit.py (+36/-20)
openlp/core/ui/themeform.py (+7/-6)
openlp/core/ui/thememanager.py (+12/-9)
openlp/core/ui/themewizard.py (+2/-0)
openlp/plugins/bibles/lib/importers/http.py (+2/-2)
openlp/plugins/presentations/lib/presentationtab.py (+9/-7)
openlp/plugins/songs/forms/editsongform.py (+8/-4)
openlp/plugins/songs/forms/songimportform.py (+6/-4)
openlp/plugins/songusage/forms/songusagedetailform.py (+5/-3)
tests/functional/openlp_core_common/test_path.py (+88/-0)
tests/functional/openlp_core_lib/test_file_dialog.py (+5/-56)
tests/functional/openlp_core_lib/test_lib.py (+35/-4)
tests/functional/openlp_core_ui/test_themeform.py (+2/-1)
tests/functional/openlp_core_ui_lib/test_filedialog.py (+188/-0)
tests/functional/openlp_core_ui_lib/test_pathedit.py (+45/-58)
To merge this branch: bzr merge lp:~phill-ridout/openlp/pathlib2
Reviewer Review Type Date Requested Status
Tim Bentley Approve
Raoul Snyman Approve
Review via email: mp+328825@code.launchpad.net

This proposal supersedes a proposal from 2017-08-09.

Description of the change

Definitely ready for merging, unless, of course you guys find some more issues!

Part 2

Changed the pathedit widget over to using pathlib
Added a 'patched' file dialog
Added a few utility methods

lp:~phill-ridout/openlp/pathlib2 (revision 2763)
[SUCCESS] https://ci.openlp.io/job/Branch-01-Pull/2125/
[SUCCESS] https://ci.openlp.io/job/Branch-02-Functional-Tests/2033/
[SUCCESS] https://ci.openlp.io/job/Branch-03-Interface-Tests/1938/
[SUCCESS] https://ci.openlp.io/job/Branch-04a-Code_Analysis/1315/
[SUCCESS] https://ci.openlp.io/job/Branch-04b-Test_Coverage/1157/
[SUCCESS] https://ci.openlp.io/job/Branch-04c-Code_Analysis2/287/
[FAILURE] https://ci.openlp.io/job/Branch-05-AppVeyor-Tests/132/
Stopping after failure

To post a comment you must log in.
Revision history for this message
Tim Bentley (trb143) wrote : Posted in a previous version of this proposal

Trunk is fine

Advanced Tab
Traceback (most recent call last):
  File "/home/tim/Projects/OpenLP/openlp/pathlib2/openlp/core/ui/mainwindow.py", line 810, in on_settings_configure_iem_clicked
    self.settings_form.exec()
  File "/home/tim/Projects/OpenLP/openlp/pathlib2/openlp/core/ui/settingsform.py", line 70, in exec
    self.insert_tab(self.general_tab)
  File "/home/tim/Projects/OpenLP/openlp/pathlib2/openlp/core/ui/settingsform.py", line 89, in insert_tab
    log.debug('Inserting {text} tab'.format(text=tab_widget.tab_title))
AttributeError: 'NoneType' object has no attribute 'tab_title'

Theme Edit with image
Traceback (most recent call last):
  File "/home/tim/Projects/OpenLP/openlp/pathlib2/openlp/core/ui/thememanager.py", line 326, in on_edit_theme
    self.theme_form.exec(True)
  File "/home/tim/Projects/OpenLP/openlp/pathlib2/openlp/core/ui/themeform.py", line 279, in exec
    self.set_defaults()
  File "/home/tim/Projects/OpenLP/openlp/pathlib2/openlp/core/ui/themeform.py", line 109, in set_defaults
    self.set_background_page_values()
  File "/home/tim/Projects/OpenLP/openlp/pathlib2/openlp/core/ui/themeform.py", line 322, in set_background_page_values
    self.image_path_edit.path = path_to_str(self.theme.background_filename)
  File "/home/tim/Projects/OpenLP/openlp/pathlib2/openlp/core/common/path.py", line 37, in path_to_str
    raise TypeError('parameter \'path\' must be of type Path or NoneType')
TypeError: parameter 'path' must be of type Path or NoneType

review: Needs Fixing
Revision history for this message
Raoul Snyman (raoul-snyman) wrote : Posted in a previous version of this proposal

We already have a custom file dialog, why not extend that one rather than write a completely new class?

Revision history for this message
Raoul Snyman (raoul-snyman) wrote : Posted in a previous version of this proposal

I've added some inline comments for some things I think need to be fixed up. Mostly small stuff.

review: Needs Fixing
Revision history for this message
Raoul Snyman (raoul-snyman) wrote :

Looks OK to me.

review: Approve
Revision history for this message
Tim Bentley (trb143) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'openlp/core/common/path.py'
2--- openlp/core/common/path.py 1970-01-01 00:00:00 +0000
3+++ openlp/core/common/path.py 2017-08-10 06:57:18 +0000
4@@ -0,0 +1,61 @@
5+# -*- coding: utf-8 -*-
6+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
7+
8+###############################################################################
9+# OpenLP - Open Source Lyrics Projection #
10+# --------------------------------------------------------------------------- #
11+# Copyright (c) 2008-2017 OpenLP Developers #
12+# --------------------------------------------------------------------------- #
13+# This program is free software; you can redistribute it and/or modify it #
14+# under the terms of the GNU General Public License as published by the Free #
15+# Software Foundation; version 2 of the License. #
16+# #
17+# This program is distributed in the hope that it will be useful, but WITHOUT #
18+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
19+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
20+# more details. #
21+# #
22+# You should have received a copy of the GNU General Public License along #
23+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
24+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
25+###############################################################################
26+
27+from pathlib import Path
28+
29+
30+def path_to_str(path):
31+ """
32+ A utility function to convert a Path object or NoneType to a string equivalent.
33+
34+ :param path: The value to convert to a string
35+ :type: pathlib.Path or None
36+
37+ :return: An empty string if :param:`path` is None, else a string representation of the :param:`path`
38+ :rtype: str
39+ """
40+ if not isinstance(path, Path) and path is not None:
41+ raise TypeError('parameter \'path\' must be of type Path or NoneType')
42+ if path is None:
43+ return ''
44+ else:
45+ return str(path)
46+
47+
48+def str_to_path(string):
49+ """
50+ A utility function to convert a str object to a Path or NoneType.
51+
52+ This function is of particular use because initating a Path object with an empty string causes the Path object to
53+ point to the current working directory.
54+
55+ :param string: The string to convert
56+ :type string: str
57+
58+ :return: None if :param:`string` is empty, or a Path object representation of :param:`string`
59+ :rtype: pathlib.Path or None
60+ """
61+ if not isinstance(string, str):
62+ raise TypeError('parameter \'string\' must be of type str')
63+ if string == '':
64+ return None
65+ return Path(string)
66
67=== modified file 'openlp/core/lib/__init__.py'
68--- openlp/core/lib/__init__.py 2017-08-06 07:23:26 +0000
69+++ openlp/core/lib/__init__.py 2017-08-10 06:57:18 +0000
70@@ -608,8 +608,42 @@
71 return list_to_string
72
73
74+def replace_params(args, kwargs, params):
75+ """
76+ Apply a transformation function to the specified args or kwargs
77+
78+ :param args: Positional arguments
79+ :type args: (,)
80+
81+ :param kwargs: Key Word arguments
82+ :type kwargs: dict
83+
84+ :param params: A tuple of tuples with the position and the key word to replace.
85+ :type params: ((int, str, path_to_str),)
86+
87+ :return: The modified positional and keyword arguments
88+ :rtype: (tuple, dict)
89+
90+
91+ Usage:
92+ Take a method with the following signature, and assume we which to apply the str function to arg2:
93+ def method(arg1=None, arg2=None, arg3=None)
94+
95+ As arg2 can be specified postitionally as the second argument (1 with a zero index) or as a keyword, the we
96+ would call this function as follows:
97+
98+ replace_params(args, kwargs, ((1, 'arg2', str),))
99+ """
100+ args = list(args)
101+ for position, key_word, transform in params:
102+ if len(args) > position:
103+ args[position] = transform(args[position])
104+ elif key_word in kwargs:
105+ kwargs[key_word] = transform(kwargs[key_word])
106+ return tuple(args), kwargs
107+
108+
109 from .exceptions import ValidationError
110-from .filedialog import FileDialog
111 from .screen import ScreenList
112 from .formattingtags import FormattingTags
113 from .plugin import PluginStatus, StringContent, Plugin
114
115=== removed file 'openlp/core/lib/filedialog.py'
116--- openlp/core/lib/filedialog.py 2016-12-31 11:01:36 +0000
117+++ openlp/core/lib/filedialog.py 1970-01-01 00:00:00 +0000
118@@ -1,58 +0,0 @@
119-# -*- coding: utf-8 -*-
120-# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
121-
122-###############################################################################
123-# OpenLP - Open Source Lyrics Projection #
124-# --------------------------------------------------------------------------- #
125-# Copyright (c) 2008-2017 OpenLP Developers #
126-# --------------------------------------------------------------------------- #
127-# This program is free software; you can redistribute it and/or modify it #
128-# under the terms of the GNU General Public License as published by the Free #
129-# Software Foundation; version 2 of the License. #
130-# #
131-# This program is distributed in the hope that it will be useful, but WITHOUT #
132-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
133-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
134-# more details. #
135-# #
136-# You should have received a copy of the GNU General Public License along #
137-# with this program; if not, write to the Free Software Foundation, Inc., 59 #
138-# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
139-###############################################################################
140-"""
141-Provide a work around for a bug in QFileDialog <https://bugs.launchpad.net/openlp/+bug/1209515>
142-"""
143-import logging
144-import os
145-from urllib import parse
146-
147-from PyQt5 import QtWidgets
148-
149-from openlp.core.common import UiStrings
150-
151-log = logging.getLogger(__name__)
152-
153-
154-class FileDialog(QtWidgets.QFileDialog):
155- """
156- Subclass QFileDialog to work round a bug
157- """
158- @staticmethod
159- def getOpenFileNames(parent, *args, **kwargs):
160- """
161- Reimplement getOpenFileNames to fix the way it returns some file names that url encoded when selecting multiple
162- files
163- """
164- files, filter_used = QtWidgets.QFileDialog.getOpenFileNames(parent, *args, **kwargs)
165- file_list = []
166- for file in files:
167- if not os.path.exists(file):
168- log.info('File not found. Attempting to unquote.')
169- file = parse.unquote(file)
170- if not os.path.exists(file):
171- log.error('File {text} not found.'.format(text=file))
172- QtWidgets.QMessageBox.information(parent, UiStrings().FileNotFound,
173- UiStrings().FileNotFoundMessage.format(name=file))
174- continue
175- file_list.append(file)
176- return file_list
177
178=== modified file 'openlp/core/lib/mediamanageritem.py'
179--- openlp/core/lib/mediamanageritem.py 2017-02-18 07:23:15 +0000
180+++ openlp/core/lib/mediamanageritem.py 2017-08-10 06:57:18 +0000
181@@ -26,12 +26,14 @@
182 import os
183 import re
184
185-from PyQt5 import QtCore, QtGui, QtWidgets
186+from PyQt5 import QtCore, QtWidgets
187
188 from openlp.core.common import Registry, RegistryProperties, Settings, UiStrings, translate
189-from openlp.core.lib import FileDialog, ServiceItem, StringContent, ServiceItemContext
190+from openlp.core.common.path import path_to_str, str_to_path
191+from openlp.core.lib import ServiceItem, StringContent, ServiceItemContext
192 from openlp.core.lib.searchedit import SearchEdit
193 from openlp.core.lib.ui import create_widget_action, critical_error_message_box
194+from openlp.core.ui.lib.filedialog import FileDialog
195 from openlp.core.ui.lib.listwidgetwithdnd import ListWidgetWithDnD
196 from openlp.core.ui.lib.toolbar import OpenLPToolbar
197
198@@ -309,13 +311,14 @@
199 """
200 Add a file to the list widget to make it available for showing
201 """
202- files = FileDialog.getOpenFileNames(self, self.on_new_prompt,
203- Settings().value(self.settings_section + '/last directory'),
204- self.on_new_file_masks)
205- log.info('New files(s) {files}'.format(files=files))
206- if files:
207+ file_paths, selected_filter = FileDialog.getOpenFileNames(
208+ self, self.on_new_prompt,
209+ str_to_path(Settings().value(self.settings_section + '/last directory')),
210+ self.on_new_file_masks)
211+ log.info('New files(s) {file_paths}'.format(file_paths=file_paths))
212+ if file_paths:
213 self.application.set_busy_cursor()
214- self.validate_and_load(files)
215+ self.validate_and_load([path_to_str(path) for path in file_paths])
216 self.application.set_normal_cursor()
217
218 def load_file(self, data):
219
220=== modified file 'openlp/core/ui/advancedtab.py'
221--- openlp/core/ui/advancedtab.py 2017-08-01 20:59:41 +0000
222+++ openlp/core/ui/advancedtab.py 2017-08-10 06:57:18 +0000
223@@ -30,6 +30,7 @@
224
225 from openlp.core.common import AppLocation, Settings, SlideLimits, UiStrings, translate
226 from openlp.core.common.languagemanager import format_time
227+from openlp.core.common.path import path_to_str
228 from openlp.core.lib import SettingsTab, build_icon
229 from openlp.core.ui.lib import PathEdit, PathType
230
231@@ -156,7 +157,7 @@
232 self.data_directory_new_label = QtWidgets.QLabel(self.data_directory_group_box)
233 self.data_directory_new_label.setObjectName('data_directory_current_label')
234 self.data_directory_path_edit = PathEdit(self.data_directory_group_box, path_type=PathType.Directories,
235- default_path=str(AppLocation.get_directory(AppLocation.DataDir)))
236+ default_path=AppLocation.get_directory(AppLocation.DataDir))
237 self.data_directory_layout.addRow(self.data_directory_new_label, self.data_directory_path_edit)
238 self.new_data_directory_has_files_label = QtWidgets.QLabel(self.data_directory_group_box)
239 self.new_data_directory_has_files_label.setObjectName('new_data_directory_has_files_label')
240@@ -373,7 +374,7 @@
241 self.new_data_directory_has_files_label.hide()
242 self.data_directory_cancel_button.hide()
243 # Since data location can be changed, make sure the path is present.
244- self.data_directory_path_edit.path = str(AppLocation.get_data_path())
245+ self.data_directory_path_edit.path = AppLocation.get_data_path()
246 # Don't allow data directory move if running portable.
247 if settings.value('advanced/is portable'):
248 self.data_directory_group_box.hide()
249@@ -497,12 +498,12 @@
250 'closed.').format(path=new_data_path),
251 defaultButton=QtWidgets.QMessageBox.No)
252 if answer != QtWidgets.QMessageBox.Yes:
253- self.data_directory_path_edit.path = str(AppLocation.get_data_path())
254+ self.data_directory_path_edit.path = AppLocation.get_data_path()
255 return
256 # Check if data already exists here.
257- self.check_data_overwrite(new_data_path)
258+ self.check_data_overwrite(path_to_str(new_data_path))
259 # Save the new location.
260- self.main_window.set_new_data_path(new_data_path)
261+ self.main_window.set_new_data_path(path_to_str(new_data_path))
262 self.data_directory_cancel_button.show()
263
264 def on_data_directory_copy_check_box_toggled(self):
265@@ -550,7 +551,7 @@
266 """
267 Cancel the data directory location change
268 """
269- self.data_directory_path_edit.path = str(AppLocation.get_data_path())
270+ self.data_directory_path_edit.path = AppLocation.get_data_path()
271 self.data_directory_copy_check_box.setChecked(False)
272 self.main_window.set_new_data_path(None)
273 self.main_window.set_copy_data(False)
274
275=== modified file 'openlp/core/ui/generaltab.py'
276--- openlp/core/ui/generaltab.py 2017-05-22 19:56:54 +0000
277+++ openlp/core/ui/generaltab.py 2017-08-10 06:57:18 +0000
278@@ -23,10 +23,12 @@
279 The general tab of the configuration dialog.
280 """
281 import logging
282+from pathlib import Path
283
284 from PyQt5 import QtCore, QtGui, QtWidgets
285
286 from openlp.core.common import Registry, Settings, UiStrings, translate, get_images_filter
287+from openlp.core.common.path import path_to_str, str_to_path
288 from openlp.core.lib import SettingsTab, ScreenList
289 from openlp.core.ui.lib import ColorButton, PathEdit
290
291@@ -172,7 +174,8 @@
292 self.logo_layout.setObjectName('logo_layout')
293 self.logo_file_label = QtWidgets.QLabel(self.logo_group_box)
294 self.logo_file_label.setObjectName('logo_file_label')
295- self.logo_file_path_edit = PathEdit(self.logo_group_box, default_path=':/graphics/openlp-splash-screen.png')
296+ self.logo_file_path_edit = PathEdit(self.logo_group_box,
297+ default_path=Path(':/graphics/openlp-splash-screen.png'))
298 self.logo_layout.addRow(self.logo_file_label, self.logo_file_path_edit)
299 self.logo_color_label = QtWidgets.QLabel(self.logo_group_box)
300 self.logo_color_label.setObjectName('logo_color_label')
301@@ -266,7 +269,7 @@
302 self.audio_group_box.setTitle(translate('OpenLP.GeneralTab', 'Background Audio'))
303 self.start_paused_check_box.setText(translate('OpenLP.GeneralTab', 'Start background audio paused'))
304 self.repeat_list_check_box.setText(translate('OpenLP.GeneralTab', 'Repeat track list'))
305- self.logo_file_path_edit.dialog_caption = dialog_caption = translate('OpenLP.AdvancedTab', 'Select Logo File')
306+ self.logo_file_path_edit.dialog_caption = translate('OpenLP.AdvancedTab', 'Select Logo File')
307 self.logo_file_path_edit.filters = '{text};;{names} (*)'.format(
308 text=get_images_filter(), names=UiStrings().AllFiles)
309
310@@ -291,7 +294,7 @@
311 self.auto_open_check_box.setChecked(settings.value('auto open'))
312 self.show_splash_check_box.setChecked(settings.value('show splash'))
313 self.logo_background_color = settings.value('logo background color')
314- self.logo_file_path_edit.path = settings.value('logo file')
315+ self.logo_file_path_edit.path = str_to_path(settings.value('logo file'))
316 self.logo_hide_on_startup_check_box.setChecked(settings.value('logo hide on startup'))
317 self.logo_color_button.color = self.logo_background_color
318 self.check_for_updates_check_box.setChecked(settings.value('update check'))
319@@ -325,7 +328,7 @@
320 settings.setValue('auto open', self.auto_open_check_box.isChecked())
321 settings.setValue('show splash', self.show_splash_check_box.isChecked())
322 settings.setValue('logo background color', self.logo_background_color)
323- settings.setValue('logo file', self.logo_file_path_edit.path)
324+ settings.setValue('logo file', path_to_str(self.logo_file_path_edit.path))
325 settings.setValue('logo hide on startup', self.logo_hide_on_startup_check_box.isChecked())
326 settings.setValue('update check', self.check_for_updates_check_box.isChecked())
327 settings.setValue('save prompt', self.save_check_service_check_box.isChecked())
328
329=== added file 'openlp/core/ui/lib/filedialog.py'
330--- openlp/core/ui/lib/filedialog.py 1970-01-01 00:00:00 +0000
331+++ openlp/core/ui/lib/filedialog.py 2017-08-10 06:57:18 +0000
332@@ -0,0 +1,113 @@
333+# -*- coding: utf-8 -*-
334+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
335+
336+###############################################################################
337+# OpenLP - Open Source Lyrics Projection #
338+# --------------------------------------------------------------------------- #
339+# Copyright (c) 2008-2017 OpenLP Developers #
340+# --------------------------------------------------------------------------- #
341+# This program is free software; you can redistribute it and/or modify it #
342+# under the terms of the GNU General Public License as published by the Free #
343+# Software Foundation; version 2 of the License. #
344+# #
345+# This program is distributed in the hope that it will be useful, but WITHOUT #
346+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
347+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
348+# more details. #
349+# #
350+# You should have received a copy of the GNU General Public License along #
351+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
352+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
353+###############################################################################
354+""" Patch the QFileDialog so it accepts and returns Path objects"""
355+from pathlib import Path
356+
357+from PyQt5 import QtWidgets
358+
359+from openlp.core.common.path import path_to_str, str_to_path
360+from openlp.core.lib import replace_params
361+
362+
363+class FileDialog(QtWidgets.QFileDialog):
364+ @classmethod
365+ def getExistingDirectory(cls, *args, **kwargs):
366+ """
367+ Wraps `getExistingDirectory` so that it can be called with, and return Path objects
368+
369+ :type parent: QtWidgets.QWidget or None
370+ :type caption: str
371+ :type directory: pathlib.Path
372+ :type options: QtWidgets.QFileDialog.Options
373+ :rtype: tuple[Path, str]
374+ """
375+ args, kwargs = replace_params(args, kwargs, ((2, 'directory', path_to_str),))
376+
377+ return_value = super().getExistingDirectory(*args, **kwargs)
378+
379+ # getExistingDirectory returns a str that represents the path. The string is empty if the user cancels the
380+ # dialog.
381+ return str_to_path(return_value)
382+
383+ @classmethod
384+ def getOpenFileName(cls, *args, **kwargs):
385+ """
386+ Wraps `getOpenFileName` so that it can be called with, and return Path objects
387+
388+ :type parent: QtWidgets.QWidget or None
389+ :type caption: str
390+ :type directory: pathlib.Path
391+ :type filter: str
392+ :type initialFilter: str
393+ :type options: QtWidgets.QFileDialog.Options
394+ :rtype: tuple[Path, str]
395+ """
396+ args, kwargs = replace_params(args, kwargs, ((2, 'directory', path_to_str),))
397+
398+ file_name, selected_filter = super().getOpenFileName(*args, **kwargs)
399+
400+ # getOpenFileName returns a tuple. The first item is a str that represents the path. The string is empty if
401+ # the user cancels the dialog.
402+ return str_to_path(file_name), selected_filter
403+
404+ @classmethod
405+ def getOpenFileNames(cls, *args, **kwargs):
406+ """
407+ Wraps `getOpenFileNames` so that it can be called with, and return Path objects
408+
409+ :type parent: QtWidgets.QWidget or None
410+ :type caption: str
411+ :type directory: pathlib.Path
412+ :type filter: str
413+ :type initialFilter: str
414+ :type options: QtWidgets.QFileDialog.Options
415+ :rtype: tuple[list[Path], str]
416+ """
417+ args, kwargs = replace_params(args, kwargs, ((2, 'directory', path_to_str),))
418+
419+ file_names, selected_filter = super().getOpenFileNames(*args, **kwargs)
420+
421+ # getSaveFileName returns a tuple. The first item is a list of str's that represents the path. The list is
422+ # empty if the user cancels the dialog.
423+ paths = [str_to_path(path) for path in file_names]
424+ return paths, selected_filter
425+
426+ @classmethod
427+ def getSaveFileName(cls, *args, **kwargs):
428+ """
429+ Wraps `getSaveFileName` so that it can be called with, and return Path objects
430+
431+ :type parent: QtWidgets.QWidget or None
432+ :type caption: str
433+ :type directory: pathlib.Path
434+ :type filter: str
435+ :type initialFilter: str
436+ :type options: QtWidgets.QFileDialog.Options
437+ :rtype: tuple[Path or None, str]
438+ """
439+ args, kwargs = replace_params(args, kwargs, ((2, 'directory', path_to_str),))
440+
441+ file_name, selected_filter = super().getSaveFileName(*args, **kwargs)
442+
443+ # getSaveFileName returns a tuple. The first item represents the path as a str. The string is empty if the user
444+ # cancels the dialog.
445+ return str_to_path(file_name), selected_filter
446
447=== modified file 'openlp/core/ui/lib/pathedit.py' (properties changed: +x to -x)
448--- openlp/core/ui/lib/pathedit.py 2017-07-04 23:13:51 +0000
449+++ openlp/core/ui/lib/pathedit.py 2017-08-10 06:57:18 +0000
450@@ -20,12 +20,14 @@
451 # Temple Place, Suite 330, Boston, MA 02111-1307 USA #
452 ###############################################################################
453 from enum import Enum
454-import os.path
455+from pathlib import Path
456
457 from PyQt5 import QtCore, QtWidgets
458
459 from openlp.core.common import UiStrings, translate
460+from openlp.core.common.path import path_to_str, str_to_path
461 from openlp.core.lib import build_icon
462+from openlp.core.ui.lib.filedialog import FileDialog
463
464
465 class PathType(Enum):
466@@ -38,11 +40,11 @@
467 The :class:`~openlp.core.ui.lib.pathedit.PathEdit` class subclasses QWidget to create a custom widget for use when
468 a file or directory needs to be selected.
469 """
470- pathChanged = QtCore.pyqtSignal(str)
471+ pathChanged = QtCore.pyqtSignal(Path)
472
473 def __init__(self, parent=None, path_type=PathType.Files, default_path=None, dialog_caption=None, show_revert=True):
474 """
475- Initalise the PathEdit widget
476+ Initialise the PathEdit widget
477
478 :param parent: The parent of the widget. This is just passed to the super method.
479 :type parent: QWidget or None
480@@ -51,9 +53,9 @@
481 :type dialog_caption: str
482
483 :param default_path: The default path. This is set as the path when the revert button is clicked
484- :type default_path: str
485+ :type default_path: pathlib.Path
486
487- :param show_revert: Used to determin if the 'revert button' should be visible.
488+ :param show_revert: Used to determine if the 'revert button' should be visible.
489 :type show_revert: bool
490
491 :return: None
492@@ -79,7 +81,6 @@
493 widget_layout = QtWidgets.QHBoxLayout()
494 widget_layout.setContentsMargins(0, 0, 0, 0)
495 self.line_edit = QtWidgets.QLineEdit(self)
496- self.line_edit.setText(self._path)
497 widget_layout.addWidget(self.line_edit)
498 self.browse_button = QtWidgets.QToolButton(self)
499 self.browse_button.setIcon(build_icon(':/general/general_open.png'))
500@@ -101,7 +102,7 @@
501 A property getter method to return the selected path.
502
503 :return: The selected path
504- :rtype: str
505+ :rtype: pathlib.Path
506 """
507 return self._path
508
509@@ -111,11 +112,15 @@
510 A Property setter method to set the selected path
511
512 :param path: The path to set the widget to
513- :type path: str
514+ :type path: pathlib.Path
515+
516+ :return: None
517+ :rtype: None
518 """
519 self._path = path
520- self.line_edit.setText(path)
521- self.line_edit.setToolTip(path)
522+ text = path_to_str(path)
523+ self.line_edit.setText(text)
524+ self.line_edit.setToolTip(text)
525
526 @property
527 def path_type(self):
528@@ -124,7 +129,7 @@
529 selecting a file or directory.
530
531 :return: The type selected
532- :rtype: Enum of PathEdit
533+ :rtype: PathType
534 """
535 return self._path_type
536
537@@ -133,8 +138,11 @@
538 """
539 A Property setter method to set the path type
540
541- :param path: The type of path to select
542- :type path: Enum of PathEdit
543+ :param path_type: The type of path to select
544+ :type path_type: PathType
545+
546+ :return: None
547+ :rtype: None
548 """
549 self._path_type = path_type
550 self.update_button_tool_tips()
551@@ -142,7 +150,9 @@
552 def update_button_tool_tips(self):
553 """
554 Called to update the tooltips on the buttons. This is changing path types, and when the widget is initalised
555+
556 :return: None
557+ :rtype: None
558 """
559 if self._path_type == PathType.Directories:
560 self.browse_button.setToolTip(translate('OpenLP.PathEdit', 'Browse for directory.'))
561@@ -156,21 +166,21 @@
562 A handler to handle a click on the browse button.
563
564 Show the QFileDialog and process the input from the user
565+
566 :return: None
567+ :rtype: None
568 """
569 caption = self.dialog_caption
570- path = ''
571+ path = None
572 if self._path_type == PathType.Directories:
573 if not caption:
574 caption = translate('OpenLP.PathEdit', 'Select Directory')
575- path = QtWidgets.QFileDialog.getExistingDirectory(self, caption,
576- self._path, QtWidgets.QFileDialog.ShowDirsOnly)
577+ path = FileDialog.getExistingDirectory(self, caption, self._path, FileDialog.ShowDirsOnly)
578 elif self._path_type == PathType.Files:
579 if not caption:
580 caption = self.dialog_caption = translate('OpenLP.PathEdit', 'Select File')
581- path, filter_used = QtWidgets.QFileDialog.getOpenFileName(self, caption, self._path, self.filters)
582+ path, filter_used = FileDialog.getOpenFileName(self, caption, self._path, self.filters)
583 if path:
584- path = os.path.normpath(path)
585 self.on_new_path(path)
586
587 def on_revert_button_clicked(self):
588@@ -178,16 +188,21 @@
589 A handler to handle a click on the revert button.
590
591 Set the new path to the value of the default_path instance variable.
592+
593 :return: None
594+ :rtype: None
595 """
596 self.on_new_path(self.default_path)
597
598 def on_line_edit_editing_finished(self):
599 """
600 A handler to handle when the line edit has finished being edited.
601+
602 :return: None
603+ :rtype: None
604 """
605- self.on_new_path(self.line_edit.text())
606+ path = str_to_path(self.line_edit.text())
607+ self.on_new_path(path)
608
609 def on_new_path(self, path):
610 """
611@@ -196,9 +211,10 @@
612 Emits the pathChanged Signal
613
614 :param path: The new path
615- :type path: str
616+ :type path: pathlib.Path
617
618 :return: None
619+ :rtype: None
620 """
621 if self._path != path:
622 self.path = path
623
624=== modified file 'openlp/core/ui/themeform.py'
625--- openlp/core/ui/themeform.py 2017-05-30 18:50:39 +0000
626+++ openlp/core/ui/themeform.py 2017-08-10 06:57:18 +0000
627@@ -28,6 +28,7 @@
628 from PyQt5 import QtCore, QtGui, QtWidgets
629
630 from openlp.core.common import Registry, RegistryProperties, UiStrings, translate, get_images_filter, is_not_image_file
631+from openlp.core.common.path import path_to_str, str_to_path
632 from openlp.core.lib.theme import BackgroundType, BackgroundGradientType
633 from openlp.core.lib.ui import critical_error_message_box
634 from openlp.core.ui import ThemeLayoutForm
635@@ -316,11 +317,11 @@
636 self.setField('background_type', 1)
637 elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Image):
638 self.image_color_button.color = self.theme.background_border_color
639- self.image_path_edit.path = self.theme.background_filename
640+ self.image_path_edit.path = str_to_path(self.theme.background_filename)
641 self.setField('background_type', 2)
642 elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Video):
643 self.video_color_button.color = self.theme.background_border_color
644- self.video_path_edit.path = self.theme.background_filename
645+ self.video_path_edit.path = str_to_path(self.theme.background_filename)
646 self.setField('background_type', 4)
647 elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Transparent):
648 self.setField('background_type', 3)
649@@ -448,18 +449,18 @@
650 """
651 self.theme.background_end_color = color
652
653- def on_image_path_edit_path_changed(self, filename):
654+ def on_image_path_edit_path_changed(self, file_path):
655 """
656 Background Image button pushed.
657 """
658- self.theme.background_filename = filename
659+ self.theme.background_filename = path_to_str(file_path)
660 self.set_background_page_values()
661
662- def on_video_path_edit_path_changed(self, filename):
663+ def on_video_path_edit_path_changed(self, file_path):
664 """
665 Background video button pushed.
666 """
667- self.theme.background_filename = filename
668+ self.theme.background_filename = path_to_str(file_path)
669 self.set_background_page_values()
670
671 def on_main_color_changed(self, color):
672
673=== modified file 'openlp/core/ui/thememanager.py'
674--- openlp/core/ui/thememanager.py 2017-08-03 04:21:19 +0000
675+++ openlp/core/ui/thememanager.py 2017-08-10 06:57:18 +0000
676@@ -22,7 +22,6 @@
677 """
678 The Theme Manager manages adding, deleteing and modifying of themes.
679 """
680-import json
681 import os
682 import zipfile
683 import shutil
684@@ -32,12 +31,14 @@
685
686 from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, OpenLPMixin, RegistryMixin, \
687 UiStrings, check_directory_exists, translate, is_win, get_filesystem_encoding, delete_file
688-from openlp.core.lib import FileDialog, ImageSource, ValidationError, get_text_file_string, build_icon, \
689+from openlp.core.common.path import path_to_str, str_to_path
690+from openlp.core.lib import ImageSource, ValidationError, get_text_file_string, build_icon, \
691 check_item_selected, create_thumb, validate_thumb
692 from openlp.core.lib.theme import Theme, BackgroundType
693 from openlp.core.lib.ui import critical_error_message_box, create_widget_action
694 from openlp.core.ui import FileRenameForm, ThemeForm
695 from openlp.core.ui.lib import OpenLPToolbar
696+from openlp.core.ui.lib.filedialog import FileDialog
697 from openlp.core.common.languagemanager import get_locale_key
698
699
700@@ -424,15 +425,17 @@
701 those files. This process will only load version 2 themes.
702 :param field:
703 """
704- files = FileDialog.getOpenFileNames(self,
705- translate('OpenLP.ThemeManager', 'Select Theme Import File'),
706- Settings().value(self.settings_section + '/last directory import'),
707- translate('OpenLP.ThemeManager', 'OpenLP Themes (*.otz)'))
708- self.log_info('New Themes {name}'.format(name=str(files)))
709- if not files:
710+ file_paths, selected_filter = FileDialog.getOpenFileNames(
711+ self,
712+ translate('OpenLP.ThemeManager', 'Select Theme Import File'),
713+ str_to_path(Settings().value(self.settings_section + '/last directory import')),
714+ translate('OpenLP.ThemeManager', 'OpenLP Themes (*.otz)'))
715+ self.log_info('New Themes {file_paths}'.format(file_paths=file_paths))
716+ if not file_paths:
717 return
718 self.application.set_busy_cursor()
719- for file_name in files:
720+ for file_path in file_paths:
721+ file_name = path_to_str(file_path)
722 Settings().setValue(self.settings_section + '/last directory import', str(file_name))
723 self.unzip_theme(file_name, self.path)
724 self.load_themes()
725
726=== modified file 'openlp/core/ui/themewizard.py'
727--- openlp/core/ui/themewizard.py 2017-05-22 18:22:43 +0000
728+++ openlp/core/ui/themewizard.py 2017-08-10 06:57:18 +0000
729@@ -22,6 +22,8 @@
730 """
731 The Create/Edit theme wizard
732 """
733+from pathlib import Path
734+
735 from PyQt5 import QtCore, QtGui, QtWidgets
736
737 from openlp.core.common import UiStrings, translate, is_macosx
738
739=== modified file 'openlp/plugins/bibles/lib/importers/http.py'
740--- openlp/plugins/bibles/lib/importers/http.py 2017-08-02 06:10:26 +0000
741+++ openlp/plugins/bibles/lib/importers/http.py 2017-08-10 06:57:18 +0000
742@@ -255,7 +255,7 @@
743 chapter=chapter,
744 version=version)
745 soup = get_soup_for_bible_ref(
746- 'http://biblegateway.com/passage/?{url}'.format(url=url_params),
747+ 'http://www.biblegateway.com/passage/?{url}'.format(url=url_params),
748 pre_parse_regex=r'<meta name.*?/>', pre_parse_substitute='')
749 if not soup:
750 return None
751@@ -284,7 +284,7 @@
752 """
753 log.debug('BGExtract.get_books_from_http("{version}")'.format(version=version))
754 url_params = urllib.parse.urlencode({'action': 'getVersionInfo', 'vid': '{version}'.format(version=version)})
755- reference_url = 'http://biblegateway.com/versions/?{url}#books'.format(url=url_params)
756+ reference_url = 'http://www.biblegateway.com/versions/?{url}#books'.format(url=url_params)
757 page = get_web_page(reference_url)
758 if not page:
759 send_error_message('download')
760
761=== modified file 'openlp/plugins/presentations/lib/presentationtab.py'
762--- openlp/plugins/presentations/lib/presentationtab.py 2017-05-22 18:27:40 +0000
763+++ openlp/plugins/presentations/lib/presentationtab.py 2017-08-10 06:57:18 +0000
764@@ -20,10 +20,11 @@
765 # Temple Place, Suite 330, Boston, MA 02111-1307 USA #
766 ###############################################################################
767
768-from PyQt5 import QtGui, QtWidgets
769+from PyQt5 import QtWidgets
770
771 from openlp.core.common import Settings, UiStrings, translate
772-from openlp.core.lib import SettingsTab, build_icon
773+from openlp.core.common.path import path_to_str, str_to_path
774+from openlp.core.lib import SettingsTab
775 from openlp.core.lib.ui import critical_error_message_box
776 from openlp.core.ui.lib import PathEdit
777 from openlp.plugins.presentations.lib.pdfcontroller import PdfController
778@@ -156,7 +157,7 @@
779 self.program_path_edit.setEnabled(enable_pdf_program)
780 pdf_program = Settings().value(self.settings_section + '/pdf_program')
781 if pdf_program:
782- self.program_path_edit.path = pdf_program
783+ self.program_path_edit.path = str_to_path(pdf_program)
784
785 def save(self):
786 """
787@@ -192,7 +193,7 @@
788 Settings().setValue(setting_key, self.ppt_window_check_box.checkState())
789 changed = True
790 # Save pdf-settings
791- pdf_program = self.program_path_edit.path
792+ pdf_program = path_to_str(self.program_path_edit.path)
793 enable_pdf_program = self.pdf_program_check_box.checkState()
794 # If the given program is blank disable using the program
795 if pdf_program == '':
796@@ -219,12 +220,13 @@
797 checkbox.setEnabled(controller.is_available())
798 self.set_controller_text(checkbox, controller)
799
800- def on_program_path_edit_path_changed(self, filename):
801+ def on_program_path_edit_path_changed(self, new_path):
802 """
803 Select the mudraw or ghostscript binary that should be used.
804 """
805- if filename:
806- if not PdfController.process_check_binary(filename):
807+ new_path = path_to_str(new_path)
808+ if new_path:
809+ if not PdfController.process_check_binary(new_path):
810 critical_error_message_box(UiStrings().Error,
811 translate('PresentationPlugin.PresentationTab',
812 'The program is not ghostscript or mudraw which is required.'))
813
814=== modified file 'openlp/plugins/songs/forms/editsongform.py'
815--- openlp/plugins/songs/forms/editsongform.py 2017-08-01 20:59:41 +0000
816+++ openlp/plugins/songs/forms/editsongform.py 2017-08-10 06:57:18 +0000
817@@ -28,12 +28,15 @@
818 import re
819 import os
820 import shutil
821+from pathlib import Path
822
823 from PyQt5 import QtCore, QtWidgets
824
825 from openlp.core.common import Registry, RegistryProperties, AppLocation, UiStrings, check_directory_exists, translate
826-from openlp.core.lib import FileDialog, PluginStatus, MediaType, create_separated_list
827+from openlp.core.common.path import path_to_str
828+from openlp.core.lib import PluginStatus, MediaType, create_separated_list
829 from openlp.core.lib.ui import set_case_insensitive_completer, critical_error_message_box, find_and_set_in_combo_box
830+from openlp.core.ui.lib.filedialog import FileDialog
831 from openlp.core.common.languagemanager import get_natural_key
832 from openlp.plugins.songs.lib import VerseType, clean_song
833 from openlp.plugins.songs.lib.db import Book, Song, Author, AuthorType, Topic, MediaFile, SongBookEntry
834@@ -925,9 +928,10 @@
835 Loads file(s) from the filesystem.
836 """
837 filters = '{text} (*)'.format(text=UiStrings().AllFiles)
838- file_names = FileDialog.getOpenFileNames(self, translate('SongsPlugin.EditSongForm', 'Open File(s)'), '',
839- filters)
840- for filename in file_names:
841+ file_paths, selected_filter = FileDialog.getOpenFileNames(
842+ self, translate('SongsPlugin.EditSongForm', 'Open File(s)'), Path(), filters)
843+ for file_path in file_paths:
844+ filename = path_to_str(file_path)
845 item = QtWidgets.QListWidgetItem(os.path.split(str(filename))[1])
846 item.setData(QtCore.Qt.UserRole, filename)
847 self.audio_list_widget.addItem(item)
848
849=== modified file 'openlp/plugins/songs/forms/songimportform.py'
850--- openlp/plugins/songs/forms/songimportform.py 2017-05-30 18:42:35 +0000
851+++ openlp/plugins/songs/forms/songimportform.py 2017-08-10 06:57:18 +0000
852@@ -29,8 +29,9 @@
853 from PyQt5 import QtCore, QtWidgets
854
855 from openlp.core.common import RegistryProperties, Settings, UiStrings, translate
856-from openlp.core.lib import FileDialog
857+from openlp.core.common.path import path_to_str, str_to_path
858 from openlp.core.lib.ui import critical_error_message_box
859+from openlp.core.ui.lib.filedialog import FileDialog
860 from openlp.core.ui.lib.wizard import OpenLPWizard, WizardStrings
861 from openlp.plugins.songs.lib.importer import SongFormat, SongFormatSelect
862
863@@ -237,10 +238,11 @@
864 if filters:
865 filters += ';;'
866 filters += '{text} (*)'.format(text=UiStrings().AllFiles)
867- file_names = FileDialog.getOpenFileNames(
868+ file_paths, selected_filter = FileDialog.getOpenFileNames(
869 self, title,
870- Settings().value(self.plugin.settings_section + '/last directory import'), filters)
871- if file_names:
872+ str_to_path(Settings().value(self.plugin.settings_section + '/last directory import')), filters)
873+ if file_paths:
874+ file_names = [path_to_str(file_path) for file_path in file_paths]
875 listbox.addItems(file_names)
876 Settings().setValue(self.plugin.settings_section + '/last directory import',
877 os.path.split(str(file_names[0]))[0])
878
879=== modified file 'openlp/plugins/songusage/forms/songusagedetailform.py'
880--- openlp/plugins/songusage/forms/songusagedetailform.py 2017-06-04 12:14:23 +0000
881+++ openlp/plugins/songusage/forms/songusagedetailform.py 2017-08-10 06:57:18 +0000
882@@ -27,6 +27,7 @@
883 from sqlalchemy.sql import and_
884
885 from openlp.core.common import RegistryProperties, Settings, check_directory_exists, translate
886+from openlp.core.common.path import path_to_str, str_to_path
887 from openlp.core.lib.ui import critical_error_message_box
888 from openlp.plugins.songusage.lib.db import SongUsageItem
889 from .songusagedetaildialog import Ui_SongUsageDetailDialog
890@@ -55,20 +56,21 @@
891 """
892 self.from_date_calendar.setSelectedDate(Settings().value(self.plugin.settings_section + '/from date'))
893 self.to_date_calendar.setSelectedDate(Settings().value(self.plugin.settings_section + '/to date'))
894- self.report_path_edit.path = Settings().value(self.plugin.settings_section + '/last directory export')
895+ self.report_path_edit.path = str_to_path(
896+ Settings().value(self.plugin.settings_section + '/last directory export'))
897
898 def on_report_path_edit_path_changed(self, file_path):
899 """
900 Triggered when the Directory selection button is clicked
901 """
902- Settings().setValue(self.plugin.settings_section + '/last directory export', file_path)
903+ Settings().setValue(self.plugin.settings_section + '/last directory export', path_to_str(file_path))
904
905 def accept(self):
906 """
907 Ok was triggered so lets save the data and run the report
908 """
909 log.debug('accept')
910- path = self.report_path_edit.path
911+ path = path_to_str(self.report_path_edit.path)
912 if not path:
913 self.main_window.error_message(
914 translate('SongUsagePlugin.SongUsageDetailForm', 'Output Path Not Selected'),
915
916=== added file 'tests/functional/openlp_core_common/test_path.py'
917--- tests/functional/openlp_core_common/test_path.py 1970-01-01 00:00:00 +0000
918+++ tests/functional/openlp_core_common/test_path.py 2017-08-10 06:57:18 +0000
919@@ -0,0 +1,88 @@
920+# -*- coding: utf-8 -*-
921+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
922+
923+###############################################################################
924+# OpenLP - Open Source Lyrics Projection #
925+# --------------------------------------------------------------------------- #
926+# Copyright (c) 2008-2017 OpenLP Developers #
927+# --------------------------------------------------------------------------- #
928+# This program is free software; you can redistribute it and/or modify it #
929+# under the terms of the GNU General Public License as published by the Free #
930+# Software Foundation; version 2 of the License. #
931+# #
932+# This program is distributed in the hope that it will be useful, but WITHOUT #
933+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
934+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
935+# more details. #
936+# #
937+# You should have received a copy of the GNU General Public License along #
938+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
939+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
940+###############################################################################
941+"""
942+Package to test the openlp.core.common.path package.
943+"""
944+import os
945+from pathlib import Path
946+from unittest import TestCase
947+
948+from openlp.core.common.path import path_to_str, str_to_path
949+
950+
951+class TestPath(TestCase):
952+ """
953+ Tests for the :mod:`openlp.core.common.path` module
954+ """
955+
956+ def test_path_to_str_type_error(self):
957+ """
958+ Test that `path_to_str` raises a type error when called with an invalid type
959+ """
960+ # GIVEN: The `path_to_str` function
961+ # WHEN: Calling `path_to_str` with an invalid Type
962+ # THEN: A TypeError should have been raised
963+ with self.assertRaises(TypeError):
964+ path_to_str(str())
965+
966+ def test_path_to_str_none(self):
967+ """
968+ Test that `path_to_str` correctly converts the path parameter when passed with None
969+ """
970+ # GIVEN: The `path_to_str` function
971+ # WHEN: Calling the `path_to_str` function with None
972+ result = path_to_str(None)
973+
974+ # THEN: `path_to_str` should return an empty string
975+ self.assertEqual(result, '')
976+
977+ def test_path_to_str_path_object(self):
978+ """
979+ Test that `path_to_str` correctly converts the path parameter when passed a Path object
980+ """
981+ # GIVEN: The `path_to_str` function
982+ # WHEN: Calling the `path_to_str` function with a Path object
983+ result = path_to_str(Path('test/path'))
984+
985+ # THEN: `path_to_str` should return a string representation of the Path object
986+ self.assertEqual(result, os.path.join('test', 'path'))
987+
988+ def test_str_to_path_type_error(self):
989+ """
990+ Test that `str_to_path` raises a type error when called with an invalid type
991+ """
992+ # GIVEN: The `str_to_path` function
993+ # WHEN: Calling `str_to_path` with an invalid Type
994+ # THEN: A TypeError should have been raised
995+ with self.assertRaises(TypeError):
996+ str_to_path(Path())
997+
998+ def test_str_to_path_empty_str(self):
999+ """
1000+ Test that `str_to_path` correctly converts the string parameter when passed with and empty string
1001+ """
1002+ # GIVEN: The `str_to_path` function
1003+ # WHEN: Calling the `str_to_path` function with None
1004+ result = str_to_path('')
1005+
1006+ # THEN: `path_to_str` should return None
1007+ self.assertEqual(result, None)
1008
1009=== modified file 'tests/functional/openlp_core_lib/test_file_dialog.py'
1010--- tests/functional/openlp_core_lib/test_file_dialog.py 2017-04-24 05:17:55 +0000
1011+++ tests/functional/openlp_core_lib/test_file_dialog.py 2017-08-10 06:57:18 +0000
1012@@ -20,12 +20,10 @@
1013 # Temple Place, Suite 330, Boston, MA 02111-1307 USA #
1014 ###############################################################################
1015 """
1016-Package to test the openlp.core.lib.filedialog package.
1017+Package to test the openlp.core.ui.lib.filedialog package.
1018 """
1019 from unittest import TestCase
1020-from unittest.mock import MagicMock, call, patch
1021-
1022-from openlp.core.lib.filedialog import FileDialog
1023+from unittest.mock import MagicMock, patch
1024
1025
1026 class TestFileDialog(TestCase):
1027@@ -33,9 +31,9 @@
1028 Test the functions in the :mod:`filedialog` module.
1029 """
1030 def setUp(self):
1031- self.os_patcher = patch('openlp.core.lib.filedialog.os')
1032- self.qt_gui_patcher = patch('openlp.core.lib.filedialog.QtWidgets')
1033- self.ui_strings_patcher = patch('openlp.core.lib.filedialog.UiStrings')
1034+ self.os_patcher = patch('openlp.core.ui.lib.filedialog.os')
1035+ self.qt_gui_patcher = patch('openlp.core.ui.lib.filedialog.QtWidgets')
1036+ self.ui_strings_patcher = patch('openlp.core.ui.lib.filedialog.UiStrings')
1037 self.mocked_os = self.os_patcher.start()
1038 self.mocked_qt_gui = self.qt_gui_patcher.start()
1039 self.mocked_ui_strings = self.ui_strings_patcher.start()
1040@@ -45,52 +43,3 @@
1041 self.os_patcher.stop()
1042 self.qt_gui_patcher.stop()
1043 self.ui_strings_patcher.stop()
1044-
1045- def test_get_open_file_names_canceled(self):
1046- """
1047- Test that FileDialog.getOpenFileNames() returns and empty QStringList when QFileDialog is canceled
1048- (returns an empty QStringList)
1049- """
1050- self.mocked_os.reset_mock()
1051-
1052- # GIVEN: An empty QStringList as a return value from QFileDialog.getOpenFileNames
1053- self.mocked_qt_gui.QFileDialog.getOpenFileNames.return_value = ([], [])
1054-
1055- # WHEN: FileDialog.getOpenFileNames is called
1056- result = FileDialog.getOpenFileNames(self.mocked_parent)
1057-
1058- # THEN: The returned value should be an empty QStringList and os.path.exists should not have been called
1059- assert not self.mocked_os.path.exists.called
1060- self.assertEqual(result, [],
1061- 'FileDialog.getOpenFileNames should return and empty list when QFileDialog.getOpenFileNames '
1062- 'is canceled')
1063-
1064- def test_returned_file_list(self):
1065- """
1066- Test that FileDialog.getOpenFileNames handles a list of files properly when QFileList.getOpenFileNames
1067- returns a good file name, a url encoded file name and a non-existing file
1068- """
1069- self.mocked_os.rest_mock()
1070- self.mocked_qt_gui.reset_mock()
1071-
1072- # GIVEN: A List of known values as a return value from QFileDialog.getOpenFileNames and a list of valid file
1073- # names.
1074- self.mocked_qt_gui.QFileDialog.getOpenFileNames.return_value = ([
1075- '/Valid File', '/url%20encoded%20file%20%231', '/non-existing'], [])
1076- self.mocked_os.path.exists.side_effect = lambda file_name: file_name in [
1077- '/Valid File', '/url encoded file #1']
1078- self.mocked_ui_strings().FileNotFound = 'File Not Found'
1079- self.mocked_ui_strings().FileNotFoundMessage = 'File {name} not found.\nPlease try selecting it individually.'
1080-
1081- # WHEN: FileDialog.getOpenFileNames is called
1082- result = FileDialog.getOpenFileNames(self.mocked_parent)
1083-
1084- # THEN: os.path.exists should have been called with known args. QmessageBox.information should have been
1085- # called. The returned result should correlate with the input.
1086- call_list = [call('/Valid File'), call('/url%20encoded%20file%20%231'), call('/url encoded file #1'),
1087- call('/non-existing'), call('/non-existing')]
1088- self.mocked_os.path.exists.assert_has_calls(call_list)
1089- self.mocked_qt_gui.QMessageBox.information.assert_called_with(
1090- self.mocked_parent, 'File Not Found',
1091- 'File /non-existing not found.\nPlease try selecting it individually.')
1092- self.assertEqual(result, ['/Valid File', '/url encoded file #1'], 'The returned file list is incorrect')
1093
1094=== modified file 'tests/functional/openlp_core_lib/test_lib.py'
1095--- tests/functional/openlp_core_lib/test_lib.py 2017-05-17 20:06:45 +0000
1096+++ tests/functional/openlp_core_lib/test_lib.py 2017-08-10 06:57:18 +0000
1097@@ -29,10 +29,9 @@
1098
1099 from PyQt5 import QtCore, QtGui
1100
1101-from openlp.core.lib import FormattingTags, expand_chords_for_printing
1102-from openlp.core.lib import build_icon, check_item_selected, clean_tags, create_thumb, create_separated_list, \
1103- expand_tags, get_text_file_string, image_to_byte, resize_image, str_to_bool, validate_thumb, expand_chords, \
1104- compare_chord_lyric, find_formatting_tags
1105+from openlp.core.lib import FormattingTags, build_icon, check_item_selected, clean_tags, compare_chord_lyric, \
1106+ create_separated_list, create_thumb, expand_chords, expand_chords_for_printing, expand_tags, find_formatting_tags, \
1107+ get_text_file_string, image_to_byte, replace_params, resize_image, str_to_bool, validate_thumb
1108
1109 TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'resources'))
1110
1111@@ -652,6 +651,38 @@
1112 mocked_os.stat.assert_any_call(thumb_path)
1113 assert result is False, 'The result should be False'
1114
1115+ def test_replace_params_no_params(self):
1116+ """
1117+ Test replace_params when called with and empty tuple instead of parameters to replace
1118+ """
1119+ # GIVEN: Some test data
1120+ test_args = (1, 2)
1121+ test_kwargs = {'arg3': 3, 'arg4': 4}
1122+ test_params = tuple()
1123+
1124+ # WHEN: Calling replace_params
1125+ result_args, result_kwargs = replace_params(test_args, test_kwargs, test_params)
1126+
1127+ # THEN: The positional and keyword args should not have changed
1128+ self.assertEqual(test_args, result_args)
1129+ self.assertEqual(test_kwargs, result_kwargs)
1130+
1131+ def test_replace_params_params(self):
1132+ """
1133+ Test replace_params when given a positional and a keyword argument to change
1134+ """
1135+ # GIVEN: Some test data
1136+ test_args = (1, 2)
1137+ test_kwargs = {'arg3': 3, 'arg4': 4}
1138+ test_params = ((1, 'arg2', str), (2, 'arg3', str))
1139+
1140+ # WHEN: Calling replace_params
1141+ result_args, result_kwargs = replace_params(test_args, test_kwargs, test_params)
1142+
1143+ # THEN: The positional and keyword args should have have changed
1144+ self.assertEqual(result_args, (1, '2'))
1145+ self.assertEqual(result_kwargs, {'arg3': '3', 'arg4': 4})
1146+
1147 def test_resize_thumb(self):
1148 """
1149 Test the resize_thumb() function
1150
1151=== modified file 'tests/functional/openlp_core_ui/test_themeform.py'
1152--- tests/functional/openlp_core_ui/test_themeform.py 2017-05-14 07:15:29 +0000
1153+++ tests/functional/openlp_core_ui/test_themeform.py 2017-08-10 06:57:18 +0000
1154@@ -22,6 +22,7 @@
1155 """
1156 Package to test the openlp.core.ui.themeform package.
1157 """
1158+from pathlib import Path
1159 from unittest import TestCase
1160 from unittest.mock import MagicMock, patch
1161
1162@@ -45,7 +46,7 @@
1163 self.instance.theme = MagicMock()
1164
1165 # WHEN: `on_image_path_edit_path_changed` is clicked
1166- self.instance.on_image_path_edit_path_changed('/new/pat.h')
1167+ self.instance.on_image_path_edit_path_changed(Path('/', 'new', 'pat.h'))
1168
1169 # THEN: The theme background file should be set and `set_background_page_values` should have been called
1170 self.assertEqual(self.instance.theme.background_filename, '/new/pat.h')
1171
1172=== renamed file 'tests/functional/openlp_core_ui_lib/test_color_button.py' => 'tests/functional/openlp_core_ui_lib/test_colorbutton.py'
1173=== added file 'tests/functional/openlp_core_ui_lib/test_filedialog.py'
1174--- tests/functional/openlp_core_ui_lib/test_filedialog.py 1970-01-01 00:00:00 +0000
1175+++ tests/functional/openlp_core_ui_lib/test_filedialog.py 2017-08-10 06:57:18 +0000
1176@@ -0,0 +1,188 @@
1177+import os
1178+from unittest import TestCase
1179+from unittest.mock import patch
1180+from pathlib import Path
1181+
1182+from PyQt5 import QtWidgets
1183+
1184+from openlp.core.ui.lib.filedialog import FileDialog
1185+
1186+
1187+class TestFileDialogPatches(TestCase):
1188+ """
1189+ Tests for the :mod:`openlp.core.ui.lib.filedialogpatches` module
1190+ """
1191+
1192+ def test_file_dialog(self):
1193+ """
1194+ Test that the :class:`FileDialog` instantiates correctly
1195+ """
1196+ # GIVEN: The FileDialog class
1197+ # WHEN: Creating an instance
1198+ instance = FileDialog()
1199+
1200+ # THEN: The instance should be an instance of QFileDialog
1201+ self.assertIsInstance(instance, QtWidgets.QFileDialog)
1202+
1203+ def test_get_existing_directory_user_abort(self):
1204+ """
1205+ Test that `getExistingDirectory` handles the case when the user cancels the dialog
1206+ """
1207+ # GIVEN: FileDialog with a mocked QDialog.getExistingDirectory method
1208+ # WHEN: Calling FileDialog.getExistingDirectory and the user cancels the dialog returns a empty string
1209+ with patch('PyQt5.QtWidgets.QFileDialog.getExistingDirectory', return_value=''):
1210+ result = FileDialog.getExistingDirectory()
1211+
1212+ # THEN: The result should be None
1213+ self.assertEqual(result, None)
1214+
1215+ def test_get_existing_directory_user_accepts(self):
1216+ """
1217+ Test that `getExistingDirectory` handles the case when the user accepts the dialog
1218+ """
1219+ # GIVEN: FileDialog with a mocked QDialog.getExistingDirectory method
1220+ # WHEN: Calling FileDialog.getExistingDirectory, the user chooses a file and accepts the dialog (it returns a
1221+ # string pointing to the directory)
1222+ with patch('PyQt5.QtWidgets.QFileDialog.getExistingDirectory', return_value=os.path.join('test', 'dir')):
1223+ result = FileDialog.getExistingDirectory()
1224+
1225+ # THEN: getExistingDirectory() should return a Path object pointing to the chosen file
1226+ self.assertEqual(result, Path('test', 'dir'))
1227+
1228+ def test_get_existing_directory_param_order(self):
1229+ """
1230+ Test that `getExistingDirectory` passes the parameters to `QFileDialog.getExistingDirectory` in the correct
1231+ order
1232+ """
1233+ # GIVEN: FileDialog
1234+ with patch('openlp.core.ui.lib.filedialog.QtWidgets.QFileDialog.getExistingDirectory', return_value='') \
1235+ as mocked_get_existing_directory:
1236+
1237+ # WHEN: Calling the getExistingDirectory method with all parameters set
1238+ FileDialog.getExistingDirectory('Parent', 'Caption', Path('test', 'dir'), 'Options')
1239+
1240+ # THEN: The `QFileDialog.getExistingDirectory` should have been called with the parameters in the correct
1241+ # order
1242+ mocked_get_existing_directory.assert_called_once_with('Parent', 'Caption', os.path.join('test', 'dir'),
1243+ 'Options')
1244+
1245+ def test_get_open_file_name_user_abort(self):
1246+ """
1247+ Test that `getOpenFileName` handles the case when the user cancels the dialog
1248+ """
1249+ # GIVEN: FileDialog with a mocked QDialog.getOpenFileName method
1250+ # WHEN: Calling FileDialog.getOpenFileName and the user cancels the dialog (it returns a tuple with the first
1251+ # value set as an empty string)
1252+ with patch('PyQt5.QtWidgets.QFileDialog.getOpenFileName', return_value=('', '')):
1253+ result = FileDialog.getOpenFileName()
1254+
1255+ # THEN: First value should be None
1256+ self.assertEqual(result[0], None)
1257+
1258+ def test_get_open_file_name_user_accepts(self):
1259+ """
1260+ Test that `getOpenFileName` handles the case when the user accepts the dialog
1261+ """
1262+ # GIVEN: FileDialog with a mocked QDialog.getOpenFileName method
1263+ # WHEN: Calling FileDialog.getOpenFileName, the user chooses a file and accepts the dialog (it returns a
1264+ # tuple with the first value set as an string pointing to the file)
1265+ with patch('PyQt5.QtWidgets.QFileDialog.getOpenFileName',
1266+ return_value=(os.path.join('test', 'chosen.file'), '')):
1267+ result = FileDialog.getOpenFileName()
1268+
1269+ # THEN: getOpenFileName() should return a tuple with the first value set to a Path object pointing to the
1270+ # chosen file
1271+ self.assertEqual(result[0], Path('test', 'chosen.file'))
1272+
1273+ def test_get_open_file_name_selected_filter(self):
1274+ """
1275+ Test that `getOpenFileName` does not modify the selectedFilter as returned by `QFileDialog.getOpenFileName`
1276+ """
1277+ # GIVEN: FileDialog with a mocked QDialog.get_save_file_name method
1278+ # WHEN: Calling FileDialog.getOpenFileName, and `QFileDialog.getOpenFileName` returns a known `selectedFilter`
1279+ with patch('PyQt5.QtWidgets.QFileDialog.getOpenFileName', return_value=('', 'selected filter')):
1280+ result = FileDialog.getOpenFileName()
1281+
1282+ # THEN: getOpenFileName() should return a tuple with the second value set to a the selected filter
1283+ self.assertEqual(result[1], 'selected filter')
1284+
1285+ def test_get_open_file_names_user_abort(self):
1286+ """
1287+ Test that `getOpenFileNames` handles the case when the user cancels the dialog
1288+ """
1289+ # GIVEN: FileDialog with a mocked QDialog.getOpenFileNames method
1290+ # WHEN: Calling FileDialog.getOpenFileNames and the user cancels the dialog (it returns a tuple with the first
1291+ # value set as an empty list)
1292+ with patch('PyQt5.QtWidgets.QFileDialog.getOpenFileNames', return_value=([], '')):
1293+ result = FileDialog.getOpenFileNames()
1294+
1295+ # THEN: First value should be an empty list
1296+ self.assertEqual(result[0], [])
1297+
1298+ def test_get_open_file_names_user_accepts(self):
1299+ """
1300+ Test that `getOpenFileNames` handles the case when the user accepts the dialog
1301+ """
1302+ # GIVEN: FileDialog with a mocked QDialog.getOpenFileNames method
1303+ # WHEN: Calling FileDialog.getOpenFileNames, the user chooses some files and accepts the dialog (it returns a
1304+ # tuple with the first value set as a list of strings pointing to the file)
1305+ with patch('PyQt5.QtWidgets.QFileDialog.getOpenFileNames',
1306+ return_value=([os.path.join('test', 'chosen.file1'), os.path.join('test', 'chosen.file2')], '')):
1307+ result = FileDialog.getOpenFileNames()
1308+
1309+ # THEN: getOpenFileNames() should return a tuple with the first value set to a list of Path objects pointing
1310+ # to the chosen file
1311+ self.assertEqual(result[0], [Path('test', 'chosen.file1'), Path('test', 'chosen.file2')])
1312+
1313+ def test_get_open_file_names_selected_filter(self):
1314+ """
1315+ Test that `getOpenFileNames` does not modify the selectedFilter as returned by `QFileDialog.getOpenFileNames`
1316+ """
1317+ # GIVEN: FileDialog with a mocked QDialog.getOpenFileNames method
1318+ # WHEN: Calling FileDialog.getOpenFileNames, and `QFileDialog.getOpenFileNames` returns a known
1319+ # `selectedFilter`
1320+ with patch('PyQt5.QtWidgets.QFileDialog.getOpenFileNames', return_value=([], 'selected filter')):
1321+ result = FileDialog.getOpenFileNames()
1322+
1323+ # THEN: getOpenFileNames() should return a tuple with the second value set to a the selected filter
1324+ self.assertEqual(result[1], 'selected filter')
1325+
1326+ def test_get_save_file_name_user_abort(self):
1327+ """
1328+ Test that `getSaveFileName` handles the case when the user cancels the dialog
1329+ """
1330+ # GIVEN: FileDialog with a mocked QDialog.get_save_file_name method
1331+ # WHEN: Calling FileDialog.getSaveFileName and the user cancels the dialog (it returns a tuple with the first
1332+ # value set as an empty string)
1333+ with patch('PyQt5.QtWidgets.QFileDialog.getSaveFileName', return_value=('', '')):
1334+ result = FileDialog.getSaveFileName()
1335+
1336+ # THEN: First value should be None
1337+ self.assertEqual(result[0], None)
1338+
1339+ def test_get_save_file_name_user_accepts(self):
1340+ """
1341+ Test that `getSaveFileName` handles the case when the user accepts the dialog
1342+ """
1343+ # GIVEN: FileDialog with a mocked QDialog.getSaveFileName method
1344+ # WHEN: Calling FileDialog.getSaveFileName, the user chooses a file and accepts the dialog (it returns a
1345+ # tuple with the first value set as an string pointing to the file)
1346+ with patch('PyQt5.QtWidgets.QFileDialog.getSaveFileName',
1347+ return_value=(os.path.join('test', 'chosen.file'), '')):
1348+ result = FileDialog.getSaveFileName()
1349+
1350+ # THEN: getSaveFileName() should return a tuple with the first value set to a Path object pointing to the
1351+ # chosen file
1352+ self.assertEqual(result[0], Path('test', 'chosen.file'))
1353+
1354+ def test_get_save_file_name_selected_filter(self):
1355+ """
1356+ Test that `getSaveFileName` does not modify the selectedFilter as returned by `QFileDialog.getSaveFileName`
1357+ """
1358+ # GIVEN: FileDialog with a mocked QDialog.get_save_file_name method
1359+ # WHEN: Calling FileDialog.getSaveFileName, and `QFileDialog.getSaveFileName` returns a known `selectedFilter`
1360+ with patch('PyQt5.QtWidgets.QFileDialog.getSaveFileName', return_value=('', 'selected filter')):
1361+ result = FileDialog.getSaveFileName()
1362+
1363+ # THEN: getSaveFileName() should return a tuple with the second value set to a the selected filter
1364+ self.assertEqual(result[1], 'selected filter')
1365
1366=== renamed file 'tests/functional/openlp_core_ui_lib/test_path_edit.py' => 'tests/functional/openlp_core_ui_lib/test_pathedit.py'
1367--- tests/functional/openlp_core_ui_lib/test_path_edit.py 2017-05-13 07:35:39 +0000
1368+++ tests/functional/openlp_core_ui_lib/test_pathedit.py 2017-08-10 06:57:18 +0000
1369@@ -22,12 +22,13 @@
1370 """
1371 This module contains tests for the openlp.core.ui.lib.pathedit module
1372 """
1373+import os
1374+from pathlib import Path
1375 from unittest import TestCase
1376-
1377-from PyQt5 import QtWidgets
1378+from unittest.mock import MagicMock, PropertyMock, patch
1379
1380 from openlp.core.ui.lib import PathEdit, PathType
1381-from unittest.mock import MagicMock, PropertyMock, patch
1382+from openlp.core.ui.lib.filedialog import FileDialog
1383
1384
1385 class TestPathEdit(TestCase):
1386@@ -43,11 +44,11 @@
1387 Test the `path` property getter.
1388 """
1389 # GIVEN: An instance of PathEdit with the `_path` instance variable set
1390- self.widget._path = 'getter/test/pat.h'
1391+ self.widget._path = Path('getter', 'test', 'pat.h')
1392
1393 # WHEN: Reading the `path` property
1394 # THEN: The value that we set should be returned
1395- self.assertEqual(self.widget.path, 'getter/test/pat.h')
1396+ self.assertEqual(self.widget.path, Path('getter', 'test', 'pat.h'))
1397
1398 def test_path_setter(self):
1399 """
1400@@ -57,13 +58,13 @@
1401 self.widget.line_edit = MagicMock()
1402
1403 # WHEN: Writing to the `path` property
1404- self.widget.path = 'setter/test/pat.h'
1405+ self.widget.path = Path('setter', 'test', 'pat.h')
1406
1407 # THEN: The `_path` instance variable should be set with the test data. The `line_edit` text and tooltip
1408 # should have also been set.
1409- self.assertEqual(self.widget._path, 'setter/test/pat.h')
1410- self.widget.line_edit.setToolTip.assert_called_once_with('setter/test/pat.h')
1411- self.widget.line_edit.setText.assert_called_once_with('setter/test/pat.h')
1412+ self.assertEqual(self.widget._path, Path('setter', 'test', 'pat.h'))
1413+ self.widget.line_edit.setToolTip.assert_called_once_with(os.path.join('setter', 'test', 'pat.h'))
1414+ self.widget.line_edit.setText.assert_called_once_with(os.path.join('setter', 'test', 'pat.h'))
1415
1416 def test_path_type_getter(self):
1417 """
1418@@ -125,22 +126,20 @@
1419 """
1420 # GIVEN: An instance of PathEdit with the `path_type` set to `Directories` and a mocked
1421 # QFileDialog.getExistingDirectory
1422- with patch('openlp.core.ui.lib.pathedit.QtWidgets.QFileDialog.getExistingDirectory', return_value='') as \
1423+ with patch('openlp.core.ui.lib.pathedit.FileDialog.getExistingDirectory', return_value=None) as \
1424 mocked_get_existing_directory, \
1425- patch('openlp.core.ui.lib.pathedit.QtWidgets.QFileDialog.getOpenFileName') as \
1426- mocked_get_open_file_name, \
1427- patch('openlp.core.ui.lib.pathedit.os.path.normpath') as mocked_normpath:
1428+ patch('openlp.core.ui.lib.pathedit.FileDialog.getOpenFileName') as mocked_get_open_file_name:
1429 self.widget._path_type = PathType.Directories
1430- self.widget._path = 'test/path/'
1431+ self.widget._path = Path('test', 'path')
1432
1433 # WHEN: Calling on_browse_button_clicked
1434 self.widget.on_browse_button_clicked()
1435
1436 # THEN: The FileDialog.getExistingDirectory should have been called with the default caption
1437- mocked_get_existing_directory.assert_called_once_with(self.widget, 'Select Directory', 'test/path/',
1438- QtWidgets.QFileDialog.ShowDirsOnly)
1439+ mocked_get_existing_directory.assert_called_once_with(self.widget, 'Select Directory',
1440+ Path('test', 'path'),
1441+ FileDialog.ShowDirsOnly)
1442 self.assertFalse(mocked_get_open_file_name.called)
1443- self.assertFalse(mocked_normpath.called)
1444
1445 def test_on_browse_button_clicked_directory_custom_caption(self):
1446 """
1447@@ -149,45 +148,40 @@
1448 """
1449 # GIVEN: An instance of PathEdit with the `path_type` set to `Directories` and a mocked
1450 # QFileDialog.getExistingDirectory with `default_caption` set.
1451- with patch('openlp.core.ui.lib.pathedit.QtWidgets.QFileDialog.getExistingDirectory', return_value='') as \
1452+ with patch('openlp.core.ui.lib.pathedit.FileDialog.getExistingDirectory', return_value=None) as \
1453 mocked_get_existing_directory, \
1454- patch('openlp.core.ui.lib.pathedit.QtWidgets.QFileDialog.getOpenFileName') as \
1455- mocked_get_open_file_name, \
1456- patch('openlp.core.ui.lib.pathedit.os.path.normpath') as mocked_normpath:
1457+ patch('openlp.core.ui.lib.pathedit.FileDialog.getOpenFileName') as mocked_get_open_file_name:
1458 self.widget._path_type = PathType.Directories
1459- self.widget._path = 'test/path/'
1460+ self.widget._path = Path('test', 'path')
1461 self.widget.dialog_caption = 'Directory Caption'
1462
1463 # WHEN: Calling on_browse_button_clicked
1464 self.widget.on_browse_button_clicked()
1465
1466 # THEN: The FileDialog.getExistingDirectory should have been called with the custom caption
1467- mocked_get_existing_directory.assert_called_once_with(self.widget, 'Directory Caption', 'test/path/',
1468- QtWidgets.QFileDialog.ShowDirsOnly)
1469+ mocked_get_existing_directory.assert_called_once_with(self.widget, 'Directory Caption',
1470+ Path('test', 'path'),
1471+ FileDialog.ShowDirsOnly)
1472 self.assertFalse(mocked_get_open_file_name.called)
1473- self.assertFalse(mocked_normpath.called)
1474
1475 def test_on_browse_button_clicked_file(self):
1476 """
1477 Test the `browse_button` `clicked` handler on_browse_button_clicked when the `path_type` is set to Files.
1478 """
1479 # GIVEN: An instance of PathEdit with the `path_type` set to `Files` and a mocked QFileDialog.getOpenFileName
1480- with patch('openlp.core.ui.lib.pathedit.QtWidgets.QFileDialog.getExistingDirectory') as \
1481- mocked_get_existing_directory, \
1482- patch('openlp.core.ui.lib.pathedit.QtWidgets.QFileDialog.getOpenFileName', return_value=('', '')) as \
1483- mocked_get_open_file_name, \
1484- patch('openlp.core.ui.lib.pathedit.os.path.normpath') as mocked_normpath:
1485+ with patch('openlp.core.ui.lib.pathedit.FileDialog.getExistingDirectory') as mocked_get_existing_directory, \
1486+ patch('openlp.core.ui.lib.pathedit.FileDialog.getOpenFileName', return_value=(None, '')) as \
1487+ mocked_get_open_file_name:
1488 self.widget._path_type = PathType.Files
1489- self.widget._path = 'test/pat.h'
1490+ self.widget._path = Path('test', 'pat.h')
1491
1492 # WHEN: Calling on_browse_button_clicked
1493 self.widget.on_browse_button_clicked()
1494
1495 # THEN: The FileDialog.getOpenFileName should have been called with the default caption
1496- mocked_get_open_file_name.assert_called_once_with(self.widget, 'Select File', 'test/pat.h',
1497+ mocked_get_open_file_name.assert_called_once_with(self.widget, 'Select File', Path('test', 'pat.h'),
1498 self.widget.filters)
1499 self.assertFalse(mocked_get_existing_directory.called)
1500- self.assertFalse(mocked_normpath.called)
1501
1502 def test_on_browse_button_clicked_file_custom_caption(self):
1503 """
1504@@ -196,23 +190,20 @@
1505 """
1506 # GIVEN: An instance of PathEdit with the `path_type` set to `Files` and a mocked QFileDialog.getOpenFileName
1507 # with `default_caption` set.
1508- with patch('openlp.core.ui.lib.pathedit.QtWidgets.QFileDialog.getExistingDirectory') as \
1509- mocked_get_existing_directory, \
1510- patch('openlp.core.ui.lib.pathedit.QtWidgets.QFileDialog.getOpenFileName', return_value=('', '')) as \
1511- mocked_get_open_file_name, \
1512- patch('openlp.core.ui.lib.pathedit.os.path.normpath') as mocked_normpath:
1513+ with patch('openlp.core.ui.lib.pathedit.FileDialog.getExistingDirectory') as mocked_get_existing_directory, \
1514+ patch('openlp.core.ui.lib.pathedit.FileDialog.getOpenFileName', return_value=(None, '')) as \
1515+ mocked_get_open_file_name:
1516 self.widget._path_type = PathType.Files
1517- self.widget._path = 'test/pat.h'
1518+ self.widget._path = Path('test', 'pat.h')
1519 self.widget.dialog_caption = 'File Caption'
1520
1521 # WHEN: Calling on_browse_button_clicked
1522 self.widget.on_browse_button_clicked()
1523
1524 # THEN: The FileDialog.getOpenFileName should have been called with the custom caption
1525- mocked_get_open_file_name.assert_called_once_with(self.widget, 'File Caption', 'test/pat.h',
1526+ mocked_get_open_file_name.assert_called_once_with(self.widget, 'File Caption', Path('test', 'pat.h'),
1527 self.widget.filters)
1528 self.assertFalse(mocked_get_existing_directory.called)
1529- self.assertFalse(mocked_normpath.called)
1530
1531 def test_on_browse_button_clicked_user_cancels(self):
1532 """
1533@@ -221,16 +212,14 @@
1534 """
1535 # GIVEN: An instance of PathEdit with a mocked QFileDialog.getOpenFileName which returns an empty str for the
1536 # file path.
1537- with patch('openlp.core.ui.lib.pathedit.QtWidgets.QFileDialog.getOpenFileName', return_value=('', '')) as \
1538- mocked_get_open_file_name, \
1539- patch('openlp.core.ui.lib.pathedit.os.path.normpath') as mocked_normpath:
1540+ with patch('openlp.core.ui.lib.pathedit.FileDialog.getOpenFileName', return_value=(None, '')) as \
1541+ mocked_get_open_file_name:
1542
1543 # WHEN: Calling on_browse_button_clicked
1544 self.widget.on_browse_button_clicked()
1545
1546 # THEN: normpath should not have been called
1547 self.assertTrue(mocked_get_open_file_name.called)
1548- self.assertFalse(mocked_normpath.called)
1549
1550 def test_on_browse_button_clicked_user_accepts(self):
1551 """
1552@@ -239,9 +228,8 @@
1553 """
1554 # GIVEN: An instance of PathEdit with a mocked QFileDialog.getOpenFileName which returns a str for the file
1555 # path.
1556- with patch('openlp.core.ui.lib.pathedit.QtWidgets.QFileDialog.getOpenFileName',
1557- return_value=('/test/pat.h', '')) as mocked_get_open_file_name, \
1558- patch('openlp.core.ui.lib.pathedit.os.path.normpath') as mocked_normpath, \
1559+ with patch('openlp.core.ui.lib.pathedit.FileDialog.getOpenFileName',
1560+ return_value=(Path('test', 'pat.h'), '')) as mocked_get_open_file_name, \
1561 patch.object(self.widget, 'on_new_path'):
1562
1563 # WHEN: Calling on_browse_button_clicked
1564@@ -249,7 +237,6 @@
1565
1566 # THEN: normpath and `on_new_path` should have been called
1567 self.assertTrue(mocked_get_open_file_name.called)
1568- mocked_normpath.assert_called_once_with('/test/pat.h')
1569 self.assertTrue(self.widget.on_new_path.called)
1570
1571 def test_on_revert_button_clicked(self):
1572@@ -258,13 +245,13 @@
1573 """
1574 # GIVEN: An instance of PathEdit with a mocked `on_new_path`, and the `default_path` set.
1575 with patch.object(self.widget, 'on_new_path') as mocked_on_new_path:
1576- self.widget.default_path = '/default/pat.h'
1577+ self.widget.default_path = Path('default', 'pat.h')
1578
1579 # WHEN: Calling `on_revert_button_clicked`
1580 self.widget.on_revert_button_clicked()
1581
1582 # THEN: on_new_path should have been called with the default path
1583- mocked_on_new_path.assert_called_once_with('/default/pat.h')
1584+ mocked_on_new_path.assert_called_once_with(Path('default', 'pat.h'))
1585
1586 def test_on_line_edit_editing_finished(self):
1587 """
1588@@ -272,13 +259,13 @@
1589 """
1590 # GIVEN: An instance of PathEdit with a mocked `line_edit` and `on_new_path`.
1591 with patch.object(self.widget, 'on_new_path') as mocked_on_new_path:
1592- self.widget.line_edit = MagicMock(**{'text.return_value': '/test/pat.h'})
1593+ self.widget.line_edit = MagicMock(**{'text.return_value': 'test/pat.h'})
1594
1595 # WHEN: Calling `on_line_edit_editing_finished`
1596 self.widget.on_line_edit_editing_finished()
1597
1598 # THEN: on_new_path should have been called with the path enetered in `line_edit`
1599- mocked_on_new_path.assert_called_once_with('/test/pat.h')
1600+ mocked_on_new_path.assert_called_once_with(Path('test', 'pat.h'))
1601
1602 def test_on_new_path_no_change(self):
1603 """
1604@@ -286,11 +273,11 @@
1605 """
1606 # GIVEN: An instance of PathEdit with a test path and mocked `pathChanged` signal
1607 with patch('openlp.core.ui.lib.pathedit.PathEdit.path', new_callable=PropertyMock):
1608- self.widget._path = '/old/test/pat.h'
1609+ self.widget._path = Path('/old', 'test', 'pat.h')
1610 self.widget.pathChanged = MagicMock()
1611
1612 # WHEN: Calling `on_new_path` with the same path as the existing path
1613- self.widget.on_new_path('/old/test/pat.h')
1614+ self.widget.on_new_path(Path('/old', 'test', 'pat.h'))
1615
1616 # THEN: The `pathChanged` signal should not be emitted
1617 self.assertFalse(self.widget.pathChanged.emit.called)
1618@@ -301,11 +288,11 @@
1619 """
1620 # GIVEN: An instance of PathEdit with a test path and mocked `pathChanged` signal
1621 with patch('openlp.core.ui.lib.pathedit.PathEdit.path', new_callable=PropertyMock):
1622- self.widget._path = '/old/test/pat.h'
1623+ self.widget._path = Path('/old', 'test', 'pat.h')
1624 self.widget.pathChanged = MagicMock()
1625
1626 # WHEN: Calling `on_new_path` with the a new path
1627- self.widget.on_new_path('/new/test/pat.h')
1628+ self.widget.on_new_path(Path('/new', 'test', 'pat.h'))
1629
1630 # THEN: The `pathChanged` signal should be emitted
1631- self.widget.pathChanged.emit.assert_called_once_with('/new/test/pat.h')
1632+ self.widget.pathChanged.emit.assert_called_once_with(Path('/new', 'test', 'pat.h'))