← Back to team overview

openlp-core team mailing list archive

[Merge] lp:~alisonken1/openlp/strings-plugins3 into lp:openlp

 

Ken Roberts has proposed merging lp:~alisonken1/openlp/strings-plugins3 into lp:openlp.

Commit message:
Convert strings in plugins part 3

Requested reviews:
  OpenLP Core (openlp-core)

For more details, see:
https://code.launchpad.net/~alisonken1/openlp/strings-plugins3/+merge/295912

Convert strings from python2 to python3 in plugins part 3

- Convert strings in plugins/remote
- Convert strings in plugins/songs
- Update projectordb test

--------------------------------
lp:~alisonken1/openlp/strings-plugins3 (revision 2669)
[SUCCESS] https://ci.openlp.io/job/Branch-01-Pull/1575/
[SUCCESS] https://ci.openlp.io/job/Branch-02-Functional-Tests/1486/
[SUCCESS] https://ci.openlp.io/job/Branch-03-Interface-Tests/1424/
[SUCCESS] https://ci.openlp.io/job/Branch-04a-Windows_Functional_Tests/1203/
[SUCCESS] https://ci.openlp.io/job/Branch-04b-Windows_Interface_Tests/793/
[SUCCESS] https://ci.openlp.io/job/Branch-05a-Code_Analysis/861/
[SUCCESS] https://ci.openlp.io/job/Branch-05b-Test_Coverage/729/

-- 
Your team OpenLP Core is requested to review the proposed merge of lp:~alisonken1/openlp/strings-plugins3 into lp:openlp.
=== modified file 'openlp/plugins/remotes/lib/httprouter.py'
--- openlp/plugins/remotes/lib/httprouter.py	2016-05-17 21:28:27 +0000
+++ openlp/plugins/remotes/lib/httprouter.py	2016-05-27 08:20:16 +0000
@@ -141,7 +141,8 @@
         """
         Initialise the router stack and any other variables.
         """
-        auth_code = "%s:%s" % (Settings().value('remotes/user id'), Settings().value('remotes/password'))
+        auth_code = "{user}:{password}".format(user=Settings().value('remotes/user id'),
+                                               password=Settings().value('remotes/password'))
         try:
             self.auth = base64.b64encode(auth_code)
         except TypeError:
@@ -189,7 +190,7 @@
             if self.headers['Authorization'] is None:
                 self.do_authorisation()
                 self.wfile.write(bytes('no auth header received', 'UTF-8'))
-            elif self.headers['Authorization'] == 'Basic %s' % self.auth:
+            elif self.headers['Authorization'] == 'Basic {auth}'.format(auth=self.auth):
                 self.do_http_success()
                 self.call_function(function, *args)
             else:
@@ -231,7 +232,7 @@
         for route, func in self.routes:
             match = re.match(route, url_path_split.path)
             if match:
-                log.debug('Route "%s" matched "%s"', route, url_path)
+                log.debug('Route "{route}" matched "{path}"'.format(route=route, path=url_path))
                 args = []
                 for param in match.groups():
                     args.append(param)
@@ -319,9 +320,9 @@
         stage = translate('RemotePlugin.Mobile', 'Stage View')
         live = translate('RemotePlugin.Mobile', 'Live View')
         self.template_vars = {
-            'app_title': "%s %s" % (UiStrings().OLPV2x, remote),
-            'stage_title': "%s %s" % (UiStrings().OLPV2x, stage),
-            'live_title': "%s %s" % (UiStrings().OLPV2x, live),
+            'app_title': "{main} {remote}".format(main=UiStrings().OLPV2x, remote=remote),
+            'stage_title': "{main} {stage}".format(main=UiStrings().OLPV2x, stage=stage),
+            'live_title': "{main} {live}".format(main=UiStrings().OLPV2x, live=live),
             'service_manager': translate('RemotePlugin.Mobile', 'Service Manager'),
             'slide_controller': translate('RemotePlugin.Mobile', 'Slide Controller'),
             'alerts': translate('RemotePlugin.Mobile', 'Alerts'),
@@ -354,7 +355,7 @@
         :param file_name: file name with path
         :return:
         """
-        log.debug('serve file request %s' % file_name)
+        log.debug('serve file request {name}'.format(name=file_name))
         parts = file_name.split('/')
         if len(parts) == 1:
             file_name = os.path.join(parts[0], 'stage.html')
@@ -381,10 +382,10 @@
                 content = Template(filename=path, input_encoding='utf-8', output_encoding='utf-8').render(**variables)
             else:
                 file_handle = open(path, 'rb')
-                log.debug('Opened %s' % path)
+                log.debug('Opened {path}'.format(path=path))
                 content = file_handle.read()
         except IOError:
-            log.exception('Failed to open %s' % path)
+            log.exception('Failed to open {path}'.format(path=path))
             return self.do_not_found()
         finally:
             if file_handle:
@@ -402,7 +403,7 @@
         Ultimately for i18n, this could first look for xx/file.html before falling back to file.html.
         where xx is the language, e.g. 'en'
         """
-        log.debug('serve file request %s' % file_name)
+        log.debug('serve file request {name}'.format(name=file_name))
         if not file_name:
             file_name = 'index.html'
         if '.' not in file_name:
@@ -433,7 +434,9 @@
         :param dimensions: image size
         :param controller_name: controller to be called
         """
-        log.debug('serve thumbnail %s/thumbnails%s/%s' % (controller_name, dimensions, file_name))
+        log.debug('serve thumbnail {cname}/thumbnails{dim}/{fname}'.format(cname=controller_name,
+                                                                           dim=dimensions,
+                                                                           fname=file_name))
         supported_controllers = ['presentations', 'images']
         # -1 means use the default dimension in ImageManager
         width = -1
@@ -539,7 +542,7 @@
 
         :param var: variable - not used
         """
-        log.debug("controller_text var = %s" % var)
+        log.debug("controller_text var = {var}".format(var=var))
         current_item = self.live_controller.service_item
         data = []
         if current_item:
@@ -594,7 +597,8 @@
         :param display_type: This is the type of slide controller, either ``preview`` or ``live``.
         :param action: The action to perform.
         """
-        event = getattr(self.live_controller, 'slidecontroller_%s_%s' % (display_type, action))
+        event = getattr(self.live_controller, 'slidecontroller_{display}_{action}'.format(display=display_type,
+                                                                                          action=action))
         if self.request_data:
             try:
                 data = json.loads(self.request_data)['request']['id']
@@ -623,7 +627,7 @@
 
         :param action: The action to perform.
         """
-        event = getattr(self.service_manager, 'servicemanager_%s_item' % action)
+        event = getattr(self.service_manager, 'servicemanager_{action}_item'.format(action=action))
         if self.request_data:
             try:
                 data = int(json.loads(self.request_data)['request']['id'])
@@ -680,7 +684,7 @@
             return self.do_http_error()
         plugin = self.plugin_manager.get_plugin_by_name(plugin_name)
         if plugin.status == PluginStatus.Active and plugin.media_item:
-            getattr(plugin.media_item, '%s_go_live' % plugin_name).emit([request_id, True])
+            getattr(plugin.media_item, '{name}_go_live'.format(name=plugin_name)).emit([request_id, True])
         return self.do_http_success()
 
     def add_to_service(self, plugin_name):
@@ -696,5 +700,5 @@
         plugin = self.plugin_manager.get_plugin_by_name(plugin_name)
         if plugin.status == PluginStatus.Active and plugin.media_item:
             item_id = plugin.media_item.create_item_from_id(request_id)
-            getattr(plugin.media_item, '%s_add_to_service' % plugin_name).emit([item_id, True])
+            getattr(plugin.media_item, '{name}_add_to_service'.format(name=plugin_name)).emit([item_id, True])
         self.do_http_success()

=== modified file 'openlp/plugins/remotes/lib/httpserver.py'
--- openlp/plugins/remotes/lib/httpserver.py	2016-05-17 21:28:27 +0000
+++ openlp/plugins/remotes/lib/httpserver.py	2016-05-27 08:20:16 +0000
@@ -136,11 +136,13 @@
         while loop < 4:
             try:
                 self.httpd = server_class((address, port), CustomHandler)
-                log.debug("Server started for class %s %s %d" % (server_class, address, port))
+                log.debug("Server started for class {name} {address} {port:d}".format(name=server_class,
+                                                                                      address=address,
+                                                                                      port=port))
                 break
             except OSError:
-                log.debug("failed to start http server thread state %d %s" %
-                          (loop, self.http_thread.isRunning()))
+                log.debug("failed to start http server thread state "
+                          "{loop:d} {running}".format(loop=loop, running=self.http_thread.isRunning()))
                 loop += 1
                 time.sleep(0.1)
             except:

=== modified file 'openlp/plugins/remotes/lib/remotetab.py'
--- openlp/plugins/remotes/lib/remotetab.py	2016-04-18 05:35:21 +0000
+++ openlp/plugins/remotes/lib/remotetab.py	2016-05-27 08:20:16 +0000
@@ -192,14 +192,14 @@
                                                     'Show thumbnails of non-text slides in remote and stage view.'))
         self.android_app_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Android App'))
         self.android_qr_description_label.setText(
-            translate('RemotePlugin.RemoteTab', 'Scan the QR code or click <a href="%s">download</a> to install the '
-                                                'Android app from Google Play.') %
-            'https://play.google.com/store/apps/details?id=org.openlp.android2')
+            translate('RemotePlugin.RemoteTab',
+                      'Scan the QR code or click <a href="{qr}">download</a> to install the Android app from Google '
+                      'Play.').format(qr='https://play.google.com/store/apps/details?id=org.openlp.android2'))
         self.ios_app_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'iOS App'))
         self.ios_qr_description_label.setText(
-            translate('RemotePlugin.RemoteTab', 'Scan the QR code or click <a href="%s">download</a> to install the '
-                                                'iOS app from the App Store.') %
-            'https://itunes.apple.com/app/id1096218725')
+            translate('RemotePlugin.RemoteTab',
+                      'Scan the QR code or click <a href="{qr}">download</a> to install the iOS app from the App '
+                      'Store.').format(qr='https://itunes.apple.com/app/id1096218725'))
         self.https_settings_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'HTTPS Server'))
         self.https_error_label.setText(
             translate('RemotePlugin.RemoteTab', 'Could not find an SSL certificate. The HTTPS server will not be '
@@ -217,18 +217,18 @@
         Update the display based on the data input on the screen
         """
         ip_address = self.get_ip_address(self.address_edit.text())
-        http_url = 'http://%s:%s/' % (ip_address, self.port_spin_box.value())
-        https_url = 'https://%s:%s/' % (ip_address, self.https_port_spin_box.value())
-        self.remote_url.setText('<a href="%s">%s</a>' % (http_url, http_url))
-        self.remote_https_url.setText('<a href="%s">%s</a>' % (https_url, https_url))
+        http_url = 'http://{url}:{text}/'.format(url=ip_address, text=self.port_spin_box.value())
+        https_url = 'https://{url}:{text}/'.format(url=ip_address, text=self.https_port_spin_box.value())
+        self.remote_url.setText('<a href="{url}">{url}</a>'.format(url=http_url))
+        self.remote_https_url.setText('<a href="{url}">{url}</a>'.format(url=https_url))
         http_url_temp = http_url + 'stage'
         https_url_temp = https_url + 'stage'
-        self.stage_url.setText('<a href="%s">%s</a>' % (http_url_temp, http_url_temp))
-        self.stage_https_url.setText('<a href="%s">%s</a>' % (https_url_temp, https_url_temp))
+        self.stage_url.setText('<a href="{url}">{url}</a>'.format(url=http_url_temp))
+        self.stage_https_url.setText('<a href="{url}">{url}</a>'.format(url=https_url_temp))
         http_url_temp = http_url + 'main'
         https_url_temp = https_url + 'main'
-        self.live_url.setText('<a href="%s">%s</a>' % (http_url_temp, http_url_temp))
-        self.live_https_url.setText('<a href="%s">%s</a>' % (https_url_temp, https_url_temp))
+        self.live_url.setText('<a href="{url}">{url}</a>'.format(url=http_url_temp))
+        self.live_https_url.setText('<a href="{url}">{url}</a>'.format(url=https_url_temp))
 
     def get_ip_address(self, ip_address):
         """

=== modified file 'openlp/plugins/songs/forms/duplicatesongremovalform.py'
--- openlp/plugins/songs/forms/duplicatesongremovalform.py	2016-04-22 18:25:57 +0000
+++ openlp/plugins/songs/forms/duplicatesongremovalform.py	2016-05-27 08:20:16 +0000
@@ -130,6 +130,7 @@
         Song wizard localisation.
         """
         self.setWindowTitle(translate('Wizard', 'Wizard'))
+        # TODO: Check format() using template strings
         self.title_label.setText(WizardStrings.HeaderStyle % translate('OpenLP.Ui',
                                                                        'Welcome to the Duplicate Song Removal Wizard'))
         self.information_label.setText(
@@ -148,8 +149,8 @@
         Set the wizard review page header text.
         """
         self.review_page.setTitle(
-            translate('Wizard', 'Review duplicate songs (%s/%s)') %
-                     (self.review_current_count, self.review_total_count))
+            translate('Wizard', 'Review duplicate songs ({current}/{total})').format(current=self.review_current_count,
+                                                                                     total=self.review_total_count))
 
     def custom_page_changed(self, page_id):
         """

=== modified file 'openlp/plugins/songs/forms/editsongform.py'
--- openlp/plugins/songs/forms/editsongform.py	2016-04-25 11:01:32 +0000
+++ openlp/plugins/songs/forms/editsongform.py	2016-05-27 08:20:16 +0000
@@ -50,7 +50,7 @@
     """
     Class to manage the editing of a song
     """
-    log.info('%s EditSongForm loaded', __name__)
+    log.info('{name} EditSongForm loaded'.format(name=__name__))
 
     def __init__(self, media_item, parent, manager):
         """
@@ -185,20 +185,23 @@
             verse = verse.data(QtCore.Qt.UserRole)
             if verse not in verse_names:
                 verses.append(verse)
-                verse_names.append('%s%s' % (VerseType.translated_tag(verse[0]), verse[1:]))
+                verse_names.append('{verse1}{verse2}'.format(verse1=VerseType.translated_tag(verse[0]),
+                                                             verse2=verse[1:]))
         for count, item in enumerate(order):
             if item not in verses:
                 invalid_verses.append(order_names[count])
         if invalid_verses:
             valid = create_separated_list(verse_names)
             if len(invalid_verses) > 1:
-                msg = translate('SongsPlugin.EditSongForm', 'There are no verses corresponding to "%(invalid)s". '
-                                'Valid entries are %(valid)s.\nPlease enter the verses separated by spaces.') % \
-                    {'invalid': ', '.join(invalid_verses), 'valid': valid}
+                msg = translate('SongsPlugin.EditSongForm',
+                                'There are no verses corresponding to "{invalid}". Valid entries are {valid}.\n'
+                                'Please enter the verses separated by spaces.'
+                                ).format(invalid=', '.join(invalid_verses), valid=valid)
             else:
-                msg = translate('SongsPlugin.EditSongForm', 'There is no verse corresponding to "%(invalid)s".'
-                                'Valid entries are %(valid)s.\nPlease enter the verses separated by spaces.') % \
-                    {'invalid': invalid_verses[0], 'valid': valid}
+                msg = translate('SongsPlugin.EditSongForm',
+                                'There is no verse corresponding to "{invalid}". Valid entries are {valid}.\n'
+                                'Please enter the verses separated by spaces.').format(invalid=invalid_verses[0],
+                                                                                       valid=valid)
             critical_error_message_box(title=translate('SongsPlugin.EditSongForm', 'Invalid Verse Order'),
                                        message=msg)
         return len(invalid_verses) == 0
@@ -242,23 +245,24 @@
             field = item.data(QtCore.Qt.UserRole)
             verse_tags.append(field)
             if not self._validate_tags(tags):
-                misplaced_tags.append('%s %s' % (VerseType.translated_name(field[0]), field[1:]))
+                misplaced_tags.append('{field1} {field2}'.format(field1=VerseType.translated_name(field[0]),
+                                                                 field2=field[1:]))
         if misplaced_tags:
             critical_error_message_box(
                 message=translate('SongsPlugin.EditSongForm',
-                                  'There are misplaced formatting tags in the following verses:\n\n%s\n\n'
-                                  'Please correct these tags before continuing.' % ', '.join(misplaced_tags)))
+                                  'There are misplaced formatting tags in the following verses:\n\n{tag}\n\n'
+                                  'Please correct these tags before continuing.').format(tag=', '.join(misplaced_tags)))
             return False
         for tag in verse_tags:
             if verse_tags.count(tag) > 26:
                 # lp#1310523: OpenLyrics allows only a-z variants of one verse:
                 # http://openlyrics.info/dataformat.html#verse-name
                 critical_error_message_box(message=translate(
-                    'SongsPlugin.EditSongForm', 'You have %(count)s verses named %(name)s %(number)s. '
-                                                'You can have at most 26 verses with the same name' %
-                                                {'count': verse_tags.count(tag),
-                                                 'name': VerseType.translated_name(tag[0]),
-                                                 'number': tag[1:]}))
+                    'SongsPlugin.EditSongForm',
+                    'You have {count} verses named {name} {number}. You can have at most '
+                    '26 verses with the same name').format(count=verse_tags.count(tag),
+                                                           name=VerseType.translated_name(tag[0]),
+                                                           number=tag[1:]))
                 return False
         return True
 
@@ -313,7 +317,7 @@
                 self.song.verse_order = re.sub('([' + verse.upper() + verse.lower() + '])(\W|$)',
                                                r'\g<1>1\2', self.song.verse_order)
         except:
-            log.exception('Problem processing song Lyrics \n%s', sxml.dump_xml())
+            log.exception('Problem processing song Lyrics \n{xml}'.forma(xml=sxml.dump_xml()))
             raise
 
     def keyPressEvent(self, event):
@@ -492,7 +496,7 @@
                 verse[0]['type'] = VerseType.tags[index]
                 if verse[0]['label'] == '':
                     verse[0]['label'] = '1'
-                verse_def = '%s%s' % (verse[0]['type'], verse[0]['label'])
+                verse_def = '{verse}{label}'.format(verse=verse[0]['type'], label=verse[0]['label'])
                 item = QtWidgets.QTableWidgetItem(verse[1])
                 item.setData(QtCore.Qt.UserRole, verse_def)
                 self.verse_list_widget.setItem(count, 0, item)
@@ -501,7 +505,7 @@
             for count, verse in enumerate(verses):
                 self.verse_list_widget.setRowCount(self.verse_list_widget.rowCount() + 1)
                 item = QtWidgets.QTableWidgetItem(verse)
-                verse_def = '%s%s' % (VerseType.tags[VerseType.Verse], str(count + 1))
+                verse_def = '{verse}{count:d}'.format(verse=VerseType.tags[VerseType.Verse], count=(count + 1))
                 item.setData(QtCore.Qt.UserRole, verse_def)
                 self.verse_list_widget.setItem(count, 0, item)
         if self.song.verse_order:
@@ -514,7 +518,7 @@
                 if verse_index is None:
                     verse_index = VerseType.from_tag(verse_def[0])
                 verse_tag = VerseType.translated_tags[verse_index].upper()
-                translated.append('%s%s' % (verse_tag, verse_def[1:]))
+                translated.append('{tag}{verse}'.format(tag=verse_tag, verse=verse_def[1:]))
             self.verse_order_edit.setText(' '.join(translated))
         else:
             self.verse_order_edit.setText('')
@@ -554,7 +558,7 @@
             item = self.verse_list_widget.item(row, 0)
             verse_def = item.data(QtCore.Qt.UserRole)
             verse_tag = VerseType.translated_tag(verse_def[0])
-            row_def = '%s%s' % (verse_tag, verse_def[1:])
+            row_def = '{tag}{verse}'.format(tag=verse_tag, verse=verse_def[1:])
             row_label.append(row_def)
         self.verse_list_widget.setVerticalHeaderLabels(row_label)
         self.verse_list_widget.resizeRowsToContents()
@@ -742,7 +746,7 @@
         self.verse_form.set_verse('', True)
         if self.verse_form.exec():
             after_text, verse_tag, verse_num = self.verse_form.get_verse()
-            verse_def = '%s%s' % (verse_tag, verse_num)
+            verse_def = '{tag}{number}'.format(tag=verse_tag, number=verse_num)
             item = QtWidgets.QTableWidgetItem(after_text)
             item.setData(QtCore.Qt.UserRole, verse_def)
             item.setText(after_text)
@@ -760,7 +764,7 @@
             self.verse_form.set_verse(temp_text, True, verse_id)
             if self.verse_form.exec():
                 after_text, verse_tag, verse_num = self.verse_form.get_verse()
-                verse_def = '%s%s' % (verse_tag, verse_num)
+                verse_def = '{tag}{number}'.format(tag=verse_tag, number=verse_num)
                 item.setData(QtCore.Qt.UserRole, verse_def)
                 item.setText(after_text)
                 # number of lines has changed, repaint the list moving the data
@@ -793,7 +797,7 @@
                 field = item.data(QtCore.Qt.UserRole)
                 verse_tag = VerseType.translated_name(field[0])
                 verse_num = field[1:]
-                verse_list += '---[%s:%s]---\n' % (verse_tag, verse_num)
+                verse_list += '---[{tag}:{number}]---\n'.format(tag=verse_tag, number=verse_num)
                 verse_list += item.text()
                 verse_list += '\n'
             self.verse_form.set_verse(verse_list)
@@ -828,7 +832,7 @@
                             verse_num = match.group(1)
                         else:
                             verse_num = '1'
-                        verse_def = '%s%s' % (verse_tag, verse_num)
+                        verse_def = '{tag}{number}'.format(tag=verse_tag, number=verse_num)
                     else:
                         if parts.endswith('\n'):
                             parts = parts.rstrip('\n')
@@ -919,7 +923,7 @@
         """
         Loads file(s) from the filesystem.
         """
-        filters = '%s (*)' % UiStrings().AllFiles
+        filters = '{text} (*)'.format(text=UiStrings().AllFiles)
         file_names = FileDialog.getOpenFileNames(self, translate('SongsPlugin.EditSongForm', 'Open File(s)'), '',
                                                  filters)
         for filename in file_names:
@@ -1027,7 +1031,7 @@
         for item in order_text.split():
             verse_tag = VerseType.tags[VerseType.from_translated_tag(item[0])]
             verse_num = item[1:].lower()
-            order.append('%s%s' % (verse_tag, verse_num))
+            order.append('{tag}{number}'.format(tag=verse_tag, number=verse_num))
         self.song.verse_order = ' '.join(order)
         self.song.ccli_number = self.ccli_number_edit.text()
         theme_name = self.theme_combo_box.currentText()
@@ -1082,12 +1086,12 @@
                 try:
                     os.remove(audio)
                 except:
-                    log.exception('Could not remove file: %s', audio)
+                    log.exception('Could not remove file: {audio}'.format(audio=audio))
         if not files:
             try:
                 os.rmdir(save_path)
             except OSError:
-                log.exception('Could not remove directory: %s', save_path)
+                log.exception('Could not remove directory: {path}'.format(path=save_path))
         clean_song(self.manager, self.song)
         self.manager.save_object(self.song)
         self.media_item.auto_select_id = self.song.id

=== modified file 'openlp/plugins/songs/forms/editverseform.py'
--- openlp/plugins/songs/forms/editverseform.py	2016-01-09 16:26:14 +0000
+++ openlp/plugins/songs/forms/editverseform.py	2016-05-27 08:20:16 +0000
@@ -59,7 +59,7 @@
         if self.verse_text_edit.textCursor().columnNumber() != 0:
             self.verse_text_edit.insertPlainText('\n')
         verse_tag = VerseType.translated_name(verse_tag)
-        self.verse_text_edit.insertPlainText('---[%s:%s]---\n' % (verse_tag, verse_num))
+        self.verse_text_edit.insertPlainText('---[{tag}:{number}]---\n'.format(tag=verse_tag, number=verse_num))
         self.verse_text_edit.setFocus()
 
     def on_split_button_clicked(self):
@@ -107,7 +107,7 @@
             self.verse_type_combo_box.currentIndex()]
         if not text:
             return
-        position = text.rfind('---[%s' % verse_name, 0, position)
+        position = text.rfind('---[{verse}'.format(verse=verse_name), 0, position)
         if position == -1:
             self.verse_number_box.setValue(1)
             return
@@ -124,7 +124,7 @@
                 verse_num = 1
             self.verse_number_box.setValue(verse_num)
 
-    def set_verse(self, text, single=False, tag='%s1' % VerseType.tags[VerseType.Verse]):
+    def set_verse(self, text, single=False, tag='{verse}1'.format(verse=VerseType.tags[VerseType.Verse])):
         """
         Save the verse
 
@@ -142,7 +142,7 @@
             self.insert_button.setVisible(False)
         else:
             if not text:
-                text = '---[%s:1]---\n' % VerseType.translated_names[VerseType.Verse]
+                text = '---[{tag}:1]---\n'.format(tag=VerseType.translated_names[VerseType.Verse])
             self.verse_type_combo_box.setCurrentIndex(0)
             self.verse_number_box.setValue(1)
             self.insert_button.setVisible(True)
@@ -167,5 +167,5 @@
         """
         text = self.verse_text_edit.toPlainText()
         if not text.startswith('---['):
-            text = '---[%s:1]---\n%s' % (VerseType.translated_names[VerseType.Verse], text)
+            text = '---[{tag}:1]---\n{text}'.format(tag=VerseType.translated_names[VerseType.Verse], text=text)
         return text

=== modified file 'openlp/plugins/songs/forms/mediafilesform.py'
--- openlp/plugins/songs/forms/mediafilesform.py	2016-01-09 16:26:14 +0000
+++ openlp/plugins/songs/forms/mediafilesform.py	2016-05-27 08:20:16 +0000
@@ -34,7 +34,7 @@
     """
     Class to show a list of files from the
     """
-    log.info('%s MediaFilesForm loaded', __name__)
+    log.info('{name} MediaFilesForm loaded'.format(name=__name__))
 
     def __init__(self, parent):
         super(MediaFilesForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint)

=== modified file 'openlp/plugins/songs/forms/songexportform.py'
--- openlp/plugins/songs/forms/songexportform.py	2016-04-25 11:01:32 +0000
+++ openlp/plugins/songs/forms/songexportform.py	2016-05-27 08:20:16 +0000
@@ -143,6 +143,7 @@
         Song wizard localisation.
         """
         self.setWindowTitle(translate('SongsPlugin.ExportWizardForm', 'Song Export Wizard'))
+        # TODO: Verify format() with template variables
         self.title_label.setText(WizardStrings.HeaderStyle %
                                  translate('OpenLP.Ui', 'Welcome to the Song Export Wizard'))
         self.information_label.setText(
@@ -151,7 +152,7 @@
         self.available_songs_page.setTitle(translate('SongsPlugin.ExportWizardForm', 'Select Songs'))
         self.available_songs_page.setSubTitle(translate('SongsPlugin.ExportWizardForm',
                                               'Check the songs you want to export.'))
-        self.search_label.setText('%s:' % UiStrings().Search)
+        self.search_label.setText('{text}:'.format(text=UiStrings().Search))
         self.uncheck_button.setText(translate('SongsPlugin.ExportWizardForm', 'Uncheck All'))
         self.check_button.setText(translate('SongsPlugin.ExportWizardForm', 'Check All'))
         self.export_song_page.setTitle(translate('SongsPlugin.ExportWizardForm', 'Select Directory'))
@@ -223,7 +224,7 @@
             if song.temporary:
                 continue
             authors = create_separated_list([author.display_name for author in song.authors])
-            title = '%s (%s)' % (str(song.title), authors)
+            title = '{title} ({author})'.format(title=song.title, author=authors)
             item = QtWidgets.QListWidgetItem(title)
             item.setData(QtCore.Qt.UserRole, song)
             item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsUserCheckable | QtCore.Qt.ItemIsEnabled)
@@ -257,7 +258,7 @@
                 self.progress_label.setText(translate('SongsPlugin.SongExportForm', 'Your song export failed.'))
         except OSError as ose:
             self.progress_label.setText(translate('SongsPlugin.SongExportForm', 'Your song export failed because this '
-                                                  'error occurred: %s') % ose.strerror)
+                                                  'error occurred: {error}').format(error=ose.strerror))
 
     def on_search_line_edit_changed(self, text):
         """

=== modified file 'openlp/plugins/songs/forms/songimportform.py'
--- openlp/plugins/songs/forms/songimportform.py	2016-04-22 18:25:57 +0000
+++ openlp/plugins/songs/forms/songimportform.py	2016-05-27 08:20:16 +0000
@@ -132,6 +132,7 @@
         Song wizard localisation.
         """
         self.setWindowTitle(translate('SongsPlugin.ImportWizardForm', 'Song Import Wizard'))
+        # TODO: Verify format() with template variables
         self.title_label.setText(WizardStrings.HeaderStyle % translate('OpenLP.Ui',
                                                                        'Welcome to the Song Import Wizard'))
         self.information_label.setText(
@@ -236,7 +237,7 @@
         """
         if filters:
             filters += ';;'
-        filters += '%s (*)' % UiStrings().AllFiles
+        filters += '{text} (*)'.format(text=UiStrings().AllFiles)
         file_names = FileDialog.getOpenFileNames(
             self, title,
             Settings().value(self.plugin.settings_section + '/last directory import'), filters)
@@ -271,9 +272,11 @@
         select_mode, format_name, ext_filter = SongFormat.get(this_format, 'selectMode', 'name', 'filter')
         file_path_edit = self.format_widgets[this_format]['file_path_edit']
         if select_mode == SongFormatSelect.SingleFile:
+            # TODO: Verify format() with template variables
             self.get_file_name(
                 WizardStrings.OpenTypeFile % format_name, file_path_edit, 'last directory import', ext_filter)
         elif select_mode == SongFormatSelect.SingleFolder:
+            # TODO: Verify format() with template variables
             self.get_folder(WizardStrings.OpenTypeFolder % format_name, file_path_edit, 'last directory import')
 
     def on_add_button_clicked(self):
@@ -283,6 +286,7 @@
         this_format = self.current_format
         select_mode, format_name, ext_filter, custom_title = \
             SongFormat.get(this_format, 'selectMode', 'name', 'filter', 'getFilesTitle')
+        # TODO: Verify format() with template variables
         title = custom_title if custom_title else WizardStrings.OpenTypeFile % format_name
         if select_mode == SongFormatSelect.MultipleFiles:
             self.get_files(title, self.format_widgets[this_format]['file_list_widget'], ext_filter)

=== modified file 'openlp/plugins/songs/forms/songmaintenanceform.py'
--- openlp/plugins/songs/forms/songmaintenanceform.py	2016-04-25 11:01:32 +0000
+++ openlp/plugins/songs/forms/songmaintenanceform.py	2016-05-27 08:20:16 +0000
@@ -164,7 +164,8 @@
         books = self.manager.get_all_objects(Book)
         books.sort(key=get_book_key)
         for book in books:
-            book_name = QtWidgets.QListWidgetItem('%s (%s)' % (book.name, book.publisher))
+            book_name = QtWidgets.QListWidgetItem('{name} ({publisher})'.format(name=book.name,
+                                                                                publisher=book.publisher))
             book_name.setData(QtCore.Qt.UserRole, book.id)
             self.song_books_list_widget.addItem(book_name)
 
@@ -310,11 +311,12 @@
                 else:
                     critical_error_message_box(
                         message=translate('SongsPlugin.SongMaintenanceForm', 'Could not save your changes.'))
-            elif critical_error_message_box(message=translate(
-                'SongsPlugin.SongMaintenanceForm', 'The author %s already exists. Would you like to make songs with '
-                'author %s use the existing author %s?') %
-                    (author.display_name, temp_display_name, author.display_name), parent=self, question=True) == \
-                    QtWidgets.QMessageBox.Yes:
+            elif critical_error_message_box(
+                    message=translate(
+                        'SongsPlugin.SongMaintenanceForm',
+                        'The author {original} already exists. Would you like to make songs with author {new} use the '
+                        'existing author {original}?').format(original=author.display_name, new=temp_display_name),
+                    parent=self, question=True) == QtWidgets.QMessageBox.Yes:
                 self._merge_objects(author, self.merge_authors, self.reset_authors)
             else:
                 # We restore the author's old first and last name as well as
@@ -346,9 +348,10 @@
                     critical_error_message_box(
                         message=translate('SongsPlugin.SongMaintenanceForm', 'Could not save your changes.'))
             elif critical_error_message_box(
-                message=translate('SongsPlugin.SongMaintenanceForm',
-                                  'The topic %s already exists. Would you like to make songs with topic %s use the '
-                                  'existing topic %s?') % (topic.name, temp_name, topic.name),
+                    message=translate('SongsPlugin.SongMaintenanceForm',
+                                      'The topic {original} already exists. Would you like to make songs with '
+                                      'topic {new} use the existing topic {original}?').format(original=topic.name,
+                                                                                               new=temp_name),
                     parent=self, question=True) == QtWidgets.QMessageBox.Yes:
                 self._merge_objects(topic, self.merge_topics, self.reset_topics)
             else:
@@ -384,9 +387,10 @@
                     critical_error_message_box(
                         message=translate('SongsPlugin.SongMaintenanceForm', 'Could not save your changes.'))
             elif critical_error_message_box(
-                message=translate('SongsPlugin.SongMaintenanceForm',
-                                  'The book %s already exists. Would you like to make '
-                                  'songs with book %s use the existing book %s?') % (book.name, temp_name, book.name),
+                    message=translate('SongsPlugin.SongMaintenanceForm',
+                                      'The book {original} already exists. Would you like to make songs with '
+                                      'book {new} use the existing book {original}?').format(original=book.name,
+                                                                                             new=temp_name),
                     parent=self, question=True) == QtWidgets.QMessageBox.Yes:
                 self._merge_objects(book, self.merge_song_books, self.reset_song_books)
             else:

=== modified file 'openlp/plugins/songs/forms/songselectdialog.py'
--- openlp/plugins/songs/forms/songselectdialog.py	2016-04-01 17:02:14 +0000
+++ openlp/plugins/songs/forms/songselectdialog.py	2016-05-27 08:20:16 +0000
@@ -242,7 +242,8 @@
         self.search_label.setText(translate('SongsPlugin.SongSelectForm', 'Search Text:'))
         self.search_button.setText(translate('SongsPlugin.SongSelectForm', 'Search'))
         self.stop_button.setText(translate('SongsPlugin.SongSelectForm', 'Stop'))
-        self.result_count_label.setText(translate('SongsPlugin.SongSelectForm', 'Found %s song(s)') % 0)
+        self.result_count_label.setText(translate('SongsPlugin.SongSelectForm',
+                                                  'Found {count:d} song(s)').format(count=0))
         self.logout_button.setText(translate('SongsPlugin.SongSelectForm', 'Logout'))
         self.view_button.setText(translate('SongsPlugin.SongSelectForm', 'View'))
         self.title_label.setText(translate('SongsPlugin.SongSelectForm', 'Title:'))

=== modified file 'openlp/plugins/songs/forms/songselectform.py'
--- openlp/plugins/songs/forms/songselectform.py	2016-04-14 08:18:53 +0000
+++ openlp/plugins/songs/forms/songselectform.py	2016-05-27 08:20:16 +0000
@@ -305,7 +305,8 @@
         self.search_progress_bar.setValue(0)
         self.set_progress_visible(True)
         self.search_results_widget.clear()
-        self.result_count_label.setText(translate('SongsPlugin.SongSelectForm', 'Found %s song(s)') % self.song_count)
+        self.result_count_label.setText(translate('SongsPlugin.SongSelectForm',
+                                                  'Found {count:d} song(s)').format(count=self.song_count))
         self.application.process_events()
         self.song_count = 0
         search_history = self.search_combobox.getItems()
@@ -343,7 +344,8 @@
         :param song:
         """
         self.song_count += 1
-        self.result_count_label.setText(translate('SongsPlugin.SongSelectForm', 'Found %s song(s)') % self.song_count)
+        self.result_count_label.setText(translate('SongsPlugin.SongSelectForm',
+                                                  'Found {count:d} song(s)').format(count=self.song_count))
         item_title = song['title'] + ' (' + ', '.join(song['authors']) + ')'
         song_item = QtWidgets.QListWidgetItem(item_title, self.search_results_widget)
         song_item.setData(QtCore.Qt.UserRole, song)

=== modified file 'openlp/plugins/songs/lib/__init__.py'
--- openlp/plugins/songs/lib/__init__.py	2016-04-10 20:24:07 +0000
+++ openlp/plugins/songs/lib/__init__.py	2016-05-27 08:20:16 +0000
@@ -534,11 +534,11 @@
         try:
             os.remove(media_file.file_name)
         except OSError:
-            log.exception('Could not remove file: %s', media_file.file_name)
+            log.exception('Could not remove file: {name}'.format(name=media_file.file_name))
     try:
         save_path = os.path.join(AppLocation.get_section_data_path(song_plugin.name), 'audio', str(song_id))
         if os.path.exists(save_path):
             os.rmdir(save_path)
     except OSError:
-        log.exception('Could not remove directory: %s', save_path)
+        log.exception('Could not remove directory: {path}'.format(path=save_path))
     song_plugin.manager.delete_object(Song, song_id)

=== modified file 'openlp/plugins/songs/lib/db.py'
--- openlp/plugins/songs/lib/db.py	2016-04-27 18:58:35 +0000
+++ openlp/plugins/songs/lib/db.py	2016-05-27 08:20:16 +0000
@@ -39,7 +39,7 @@
     """
     def get_display_name(self, author_type=None):
         if author_type:
-            return "%s (%s)" % (self.display_name, AuthorType.Types[author_type])
+            return "{name} ({author})".format(name=self.display_name, author=AuthorType.Types[author_type])
         return self.display_name
 
 
@@ -105,7 +105,9 @@
     Book model
     """
     def __repr__(self):
-        return '<Book id="%s" name="%s" publisher="%s" />' % (str(self.id), self.name, self.publisher)
+        return '<Book id="{myid:d}" name="{name}" publisher="{publisher}" />'.format(myid=self.id,
+                                                                                     name=self.name,
+                                                                                     publisher=self.publisher)
 
 
 class MediaFile(BaseModel):
@@ -187,7 +189,7 @@
     @staticmethod
     def get_display_name(songbook_name, entry):
         if entry:
-            return "%s #%s" % (songbook_name, entry)
+            return "{name} #{entry}".format(name=songbook_name, entry=entry)
         return songbook_name
 
 

=== modified file 'openlp/plugins/songs/lib/importer.py'
--- openlp/plugins/songs/lib/importer.py	2016-04-22 18:25:57 +0000
+++ openlp/plugins/songs/lib/importer.py	2016-05-27 08:20:16 +0000
@@ -56,13 +56,13 @@
     from .importers.songsoffellowship import SongsOfFellowshipImport
     HAS_SOF = True
 except ImportError:
-    log.exception('Error importing %s', 'SongsOfFellowshipImport')
+    log.exception('Error importing {text}'.format(text='SongsOfFellowshipImport'))
     HAS_SOF = False
 try:
     from .importers.openoffice import OpenOfficeImport
     HAS_OOO = True
 except ImportError:
-    log.exception('Error importing %s', 'OooImport')
+    log.exception('Error importing {text}'.format(text='OooImport'))
     HAS_OOO = False
 HAS_MEDIASHOUT = False
 if is_win():
@@ -70,21 +70,21 @@
         from .importers.mediashout import MediaShoutImport
         HAS_MEDIASHOUT = True
     except ImportError:
-        log.exception('Error importing %s', 'MediaShoutImport')
+        log.exception('Error importing {text}'.format(text='MediaShoutImport'))
 HAS_WORSHIPCENTERPRO = False
 if is_win():
     try:
         from .importers.worshipcenterpro import WorshipCenterProImport
         HAS_WORSHIPCENTERPRO = True
     except ImportError:
-        log.exception('Error importing %s', 'WorshipCenterProImport')
+        log.exception('Error importing {text}'.format(text='WorshipCenterProImport'))
 HAS_OPSPRO = False
 if is_win():
     try:
         from .importers.opspro import OPSProImport
         HAS_OPSPRO = True
     except ImportError:
-        log.exception('Error importing %s', 'OPSProImport')
+        log.exception('Error importing {text}'.format(text='OPSProImport'))
 
 
 class SongFormatSelect(object):
@@ -198,7 +198,7 @@
             'class': OpenLyricsImport,
             'name': 'OpenLyrics',
             'prefix': 'openLyrics',
-            'filter': '%s (*.xml)' % translate('SongsPlugin.ImportWizardForm', 'OpenLyrics Files'),
+            'filter': '{text} (*.xml)'.format(text=translate('SongsPlugin.ImportWizardForm', 'OpenLyrics Files')),
             'comboBoxText': translate('SongsPlugin.ImportWizardForm', 'OpenLyrics or OpenLP 2 Exported Song')
         },
         OpenLP2: {
@@ -206,7 +206,7 @@
             'name': UiStrings().OLPV2,
             'prefix': 'openLP2',
             'selectMode': SongFormatSelect.SingleFile,
-            'filter': '%s (*.sqlite)' % (translate('SongsPlugin.ImportWizardForm', 'OpenLP 2 Databases'))
+            'filter': '{text} (*.sqlite)'.format(text=translate('SongsPlugin.ImportWizardForm', 'OpenLP 2 Databases'))
         },
         Generic: {
             'name': translate('SongsPlugin.ImportWizardForm', 'Generic Document/Presentation'),
@@ -221,46 +221,50 @@
             'class': CCLIFileImport,
             'name': 'CCLI/SongSelect',
             'prefix': 'ccli',
-            'filter': '%s (*.usr *.txt *.bin)' % translate('SongsPlugin.ImportWizardForm', 'CCLI SongSelect Files')
+            'filter': '{text} (*.usr *.txt *.bin)'.format(text=translate('SongsPlugin.ImportWizardForm',
+                                                                         'CCLI SongSelect Files'))
         },
         DreamBeam: {
             'class': DreamBeamImport,
             'name': 'DreamBeam',
             'prefix': 'dreamBeam',
-            'filter': '%s (*.xml)' % translate('SongsPlugin.ImportWizardForm', 'DreamBeam Song Files')
+            'filter': '{text} (*.xml)'.format(text=translate('SongsPlugin.ImportWizardForm', 'DreamBeam Song Files'))
         },
         EasySlides: {
             'class': EasySlidesImport,
             'name': 'EasySlides',
             'prefix': 'easySlides',
             'selectMode': SongFormatSelect.SingleFile,
-            'filter': '%s (*.xml)' % translate('SongsPlugin.ImportWizardForm', 'EasySlides XML File')
+            'filter': '{text} (*.xml)'.format(text=translate('SongsPlugin.ImportWizardForm', 'EasySlides XML File'))
         },
         EasyWorshipDB: {
             'class': EasyWorshipSongImport,
             'name': 'EasyWorship Song Database',
             'prefix': 'ew',
             'selectMode': SongFormatSelect.SingleFile,
-            'filter': '%s (*.db)' % translate('SongsPlugin.ImportWizardForm', 'EasyWorship Song Database')
+            'filter': '{text} (*.db)'.format(text=translate('SongsPlugin.ImportWizardForm',
+                                                            'EasyWorship Song Database'))
         },
         EasyWorshipService: {
             'class': EasyWorshipSongImport,
             'name': 'EasyWorship Service File',
             'prefix': 'ew',
             'selectMode': SongFormatSelect.SingleFile,
-            'filter': '%s (*.ews)' % translate('SongsPlugin.ImportWizardForm', 'EasyWorship Service File')
+            'filter': '{text} (*.ews)'.format(text=translate('SongsPlugin.ImportWizardForm',
+                                                             'EasyWorship Service File'))
         },
         FoilPresenter: {
             'class': FoilPresenterImport,
             'name': 'Foilpresenter',
             'prefix': 'foilPresenter',
-            'filter': '%s (*.foil)' % translate('SongsPlugin.ImportWizardForm', 'Foilpresenter Song Files')
+            'filter': '{text} (*.foil)'.format(text=translate('SongsPlugin.ImportWizardForm',
+                                                              'Foilpresenter Song Files'))
         },
         Lyrix: {
             'class': LyrixImport,
             'name': 'LyriX',
             'prefix': 'lyrix',
-            'filter': '%s (*.txt)' % translate('SongsPlugin.ImportWizardForm', 'LyriX Files'),
+            'filter': '{text} (*.txt)'.format(text=translate('SongsPlugin.ImportWizardForm', 'LyriX Files')),
             'comboBoxText': translate('SongsPlugin.ImportWizardForm', 'LyriX (Exported TXT-files)')
         },
         MediaShout: {
@@ -268,7 +272,7 @@
             'prefix': 'mediaShout',
             'canDisable': True,
             'selectMode': SongFormatSelect.SingleFile,
-            'filter': '%s (*.mdb)' % translate('SongsPlugin.ImportWizardForm', 'MediaShout Database'),
+            'filter': '{text} (*.mdb)'.format(text=translate('SongsPlugin.ImportWizardForm', 'MediaShout Database')),
             'disabledLabelText': translate('SongsPlugin.ImportWizardForm',
                                            'The MediaShout importer is only supported on Windows. It has '
                                            'been disabled due to a missing Python module. If you want to '
@@ -285,7 +289,7 @@
             'prefix': 'OPSPro',
             'canDisable': True,
             'selectMode': SongFormatSelect.SingleFile,
-            'filter': '%s (*.mdb)' % translate('SongsPlugin.ImportWizardForm', 'OPS Pro database'),
+            'filter': '{text} (*.mdb)'.format(text=translate('SongsPlugin.ImportWizardForm', 'OPS Pro database')),
             'disabledLabelText': translate('SongsPlugin.ImportWizardForm',
                                            'The OPS Pro importer is only supported on Windows. It has been '
                                            'disabled due to a missing Python module. If you want to use this '
@@ -295,7 +299,7 @@
             'class': PowerPraiseImport,
             'name': 'PowerPraise',
             'prefix': 'powerPraise',
-            'filter': '%s (*.ppl)' % translate('SongsPlugin.ImportWizardForm', 'PowerPraise Song Files')
+            'filter': '{text} (*.ppl)'.format(text=translate('SongsPlugin.ImportWizardForm', 'PowerPraise Song Files'))
         },
         PowerSong: {
             'class': PowerSongImport,
@@ -309,26 +313,29 @@
             'class': PresentationManagerImport,
             'name': 'PresentationManager',
             'prefix': 'presentationManager',
-            'filter': '%s (*.sng)' % translate('SongsPlugin.ImportWizardForm', 'PresentationManager Song Files')
+            'filter': '{text} (*.sng)'.format(text=translate('SongsPlugin.ImportWizardForm',
+                                                             'PresentationManager Song Files'))
         },
         ProPresenter: {
             'class': ProPresenterImport,
             'name': 'ProPresenter 4, 5 and 6',
             'prefix': 'proPresenter',
-            'filter': '%s (*.pro4 *.pro5 *.pro6)' % translate('SongsPlugin.ImportWizardForm', 'ProPresenter Song Files')
+            'filter': '{text} (*.pro4 *.pro5 *.pro6)'.format(text=translate('SongsPlugin.ImportWizardForm',
+                                                                            'ProPresenter Song Files'))
         },
         SongBeamer: {
             'class': SongBeamerImport,
             'name': 'SongBeamer',
             'prefix': 'songBeamer',
-            'filter': '%s (*.sng)' % translate('SongsPlugin.ImportWizardForm', 'SongBeamer Files')
+            'filter': '{text} (*.sng)'.format(text=translate('SongsPlugin.ImportWizardForm',
+                                                             'SongBeamer Files'))
         },
         SongPro: {
             'class': SongProImport,
             'name': 'SongPro',
             'prefix': 'songPro',
             'selectMode': SongFormatSelect.SingleFile,
-            'filter': '%s (*.txt)' % translate('SongsPlugin.ImportWizardForm', 'SongPro Text Files'),
+            'filter': '{text} (*.txt)'.format(text=translate('SongsPlugin.ImportWizardForm', 'SongPro Text Files')),
             'comboBoxText': translate('SongsPlugin.ImportWizardForm', 'SongPro (Export File)'),
             'descriptionText': translate('SongsPlugin.ImportWizardForm',
                                          'In SongPro, export your songs using the File -> Export menu')
@@ -337,13 +344,15 @@
             'class': SongShowPlusImport,
             'name': 'SongShow Plus',
             'prefix': 'songShowPlus',
-            'filter': '%s (*.sbsong)' % translate('SongsPlugin.ImportWizardForm', 'SongShow Plus Song Files')
+            'filter': '{text} (*.sbsong)'.format(text=translate('SongsPlugin.ImportWizardForm',
+                                                                'SongShow Plus Song Files'))
         },
         SongsOfFellowship: {
             'name': 'Songs of Fellowship',
             'prefix': 'songsOfFellowship',
             'canDisable': True,
-            'filter': '%s (*.rtf)' % translate('SongsPlugin.ImportWizardForm', 'Songs Of Fellowship Song Files'),
+            'filter': '{text} (*.rtf)'.format(text=translate('SongsPlugin.ImportWizardForm',
+                                                             'Songs Of Fellowship Song Files')),
             'disabledLabelText': translate('SongsPlugin.ImportWizardForm',
                                            'The Songs of Fellowship importer has been disabled because '
                                            'OpenLP cannot access OpenOffice or LibreOffice.')
@@ -352,30 +361,33 @@
             'class': SundayPlusImport,
             'name': 'SundayPlus',
             'prefix': 'sundayPlus',
-            'filter': '%s (*.ptf)' % translate('SongsPlugin.ImportWizardForm', 'SundayPlus Song Files')
+            'filter': '{text} (*.ptf)'.format(text=translate('SongsPlugin.ImportWizardForm', 'SundayPlus Song Files'))
         },
         VideoPsalm: {
             'class': VideoPsalmImport,
             'name': 'VideoPsalm',
             'prefix': 'videopsalm',
             'selectMode': SongFormatSelect.SingleFile,
-            'filter': '%s (*.json)' % translate('SongsPlugin.ImportWizardForm', 'VideoPsalm Files'),
+            'filter': '{text} (*.json)'.format(text=translate('SongsPlugin.ImportWizardForm', 'VideoPsalm Files')),
             'comboBoxText': translate('SongsPlugin.ImportWizardForm', 'VideoPsalm'),
             'descriptionText': translate('SongsPlugin.ImportWizardForm', 'The VideoPsalm songbooks are normally located'
-                                         ' in %s') % 'C:\\Users\\Public\\Documents\\VideoPsalm\\SongBooks\\'
+                                         ' in {path}').format(path='C:\\Users\\Public\\Documents\\VideoPsalm'
+                                                                   '\\SongBooks\\')
         },
         WordsOfWorship: {
             'class': WordsOfWorshipImport,
             'name': 'Words of Worship',
             'prefix': 'wordsOfWorship',
-            'filter': '%s (*.wsg *.wow-song)' % translate('SongsPlugin.ImportWizardForm', 'Words Of Worship Song Files')
+            'filter': '{text} (*.wsg *.wow-song)'.format(text=translate('SongsPlugin.ImportWizardForm',
+                                                                        'Words Of Worship Song Files'))
         },
         WorshipAssistant: {
             'class': WorshipAssistantImport,
             'name': 'Worship Assistant 0',
             'prefix': 'worshipAssistant',
             'selectMode': SongFormatSelect.SingleFile,
-            'filter': '%s (*.csv)' % translate('SongsPlugin.ImportWizardForm', 'Worship Assistant Files'),
+            'filter': '{text} (*.csv)'.format(text=translate('SongsPlugin.ImportWizardForm',
+                                                             'Worship Assistant Files')),
             'comboBoxText': translate('SongsPlugin.ImportWizardForm', 'Worship Assistant (CSV)'),
             'descriptionText': translate('SongsPlugin.ImportWizardForm',
                                          'In Worship Assistant, export your Database to a CSV file.')
@@ -385,7 +397,8 @@
             'prefix': 'worshipCenterPro',
             'canDisable': True,
             'selectMode': SongFormatSelect.SingleFile,
-            'filter': '%s (*.mdb)' % translate('SongsPlugin.ImportWizardForm', 'WorshipCenter Pro Song Files'),
+            'filter': '{text} (*.mdb)'.format(text=translate('SongsPlugin.ImportWizardForm',
+                                                             'WorshipCenter Pro Song Files')),
             'disabledLabelText': translate('SongsPlugin.ImportWizardForm',
                                            'The WorshipCenter Pro importer is only supported on Windows. It has been '
                                            'disabled due to a missing Python module. If you want to use this '

=== modified file 'openlp/plugins/songs/lib/importers/cclifile.py'
--- openlp/plugins/songs/lib/importers/cclifile.py	2015-12-31 22:46:06 +0000
+++ openlp/plugins/songs/lib/importers/cclifile.py	2016-05-27 08:20:16 +0000
@@ -58,7 +58,7 @@
         self.import_wizard.progress_bar.setMaximum(len(self.import_source))
         for filename in self.import_source:
             filename = str(filename)
-            log.debug('Importing CCLI File: %s', filename)
+            log.debug('Importing CCLI File: {name}'.format(name=filename))
             if os.path.isfile(filename):
                 detect_file = open(filename, 'rb')
                 detect_content = detect_file.read(2048)
@@ -76,17 +76,17 @@
                 infile.close()
                 ext = os.path.splitext(filename)[1]
                 if ext.lower() == '.usr' or ext.lower() == '.bin':
-                    log.info('SongSelect USR format file found: %s', filename)
+                    log.info('SongSelect USR format file found: {name}'.format(name=filename))
                     if not self.do_import_usr_file(lines):
                         self.log_error(filename)
                 elif ext.lower() == '.txt':
-                    log.info('SongSelect TEXT format file found: %s', filename)
+                    log.info('SongSelect TEXT format file found: {name}'.format(name=filename))
                     if not self.do_import_txt_file(lines):
                         self.log_error(filename)
                 else:
                     self.log_error(filename, translate('SongsPlugin.CCLIFileImport', 'The file does not have a valid '
                                                                                      'extension.'))
-                    log.info('Extension %s is not valid', filename)
+                    log.info('Extension {name} is not valid'.format(name=filename))
             if self.stop_import_flag:
                 return
 
@@ -146,7 +146,7 @@
 
         :param text_list: An array of strings containing the usr file content.
         """
-        log.debug('USR file text: %s', text_list)
+        log.debug('USR file text: {text}'.format(text=text_list))
         song_author = ''
         song_topics = ''
         for line in text_list:
@@ -193,7 +193,7 @@
             if check_first_verse_line:
                 if verse_lines[0].startswith('(PRE-CHORUS'):
                     verse_type = VerseType.tags[VerseType.PreChorus]
-                    log.debug('USR verse PRE-CHORUS: %s', verse_lines[0])
+                    log.debug('USR verse PRE-CHORUS: {lines}'.format(lines=verse_lines[0]))
                     verse_text = verse_lines[1]
                 elif verse_lines[0].startswith('(BRIDGE'):
                     verse_type = VerseType.tags[VerseType.Bridge]
@@ -248,7 +248,7 @@
                 # e.g. CCLI-Liedlizenznummer: 14 / CCLI License No. 14
 
         """
-        log.debug('TXT file text: %s', text_list)
+        log.debug('TXT file text: {text}'.format(text=text_list))
         line_number = 0
         check_first_verse_line = False
         verse_text = ''

=== modified file 'openlp/plugins/songs/lib/importers/dreambeam.py'
--- openlp/plugins/songs/lib/importers/dreambeam.py	2015-12-31 22:46:06 +0000
+++ openlp/plugins/songs/lib/importers/dreambeam.py	2016-05-27 08:20:16 +0000
@@ -90,7 +90,7 @@
                 try:
                     parsed_file = etree.parse(open(file, 'r'), parser)
                 except etree.XMLSyntaxError:
-                    log.exception('XML syntax error in file %s' % file)
+                    log.exception('XML syntax error in file {name}'.format(name=file))
                     self.log_error(file, SongStrings.XMLSyntaxError)
                     continue
                 xml = etree.tostring(parsed_file).decode()
@@ -115,15 +115,17 @@
                             verse_type = lyrics_item.get('Type')
                             verse_number = lyrics_item.get('Number')
                             verse_text = str(lyrics_item.text)
-                            self.add_verse(verse_text, ('%s%s' % (verse_type[:1], verse_number)))
+                            self.add_verse(verse_text,
+                                           '{verse}{number}'.format(verse=verse_type[:1], number=verse_number))
                     if hasattr(song_xml, 'Collection'):
                         self.song_book_name = str(song_xml.Collection.text)
                     if hasattr(song_xml, 'Number'):
                         self.song_number = str(song_xml.Number.text)
                     if hasattr(song_xml, 'Sequence'):
                         for lyrics_sequence_item in (song_xml.Sequence.iterchildren()):
-                            self.verse_order_list.append("%s%s" % (lyrics_sequence_item.get('Type')[:1],
-                                                         lyrics_sequence_item.get('Number')))
+                            item = lyrics_sequence_item.get('Type')[:1]
+                            self.verse_order_list.append("{item}{number}".format(item=item),
+                                                         lyrics_sequence_item.get('Number'))
                     if hasattr(song_xml, 'Notes'):
                         self.comments = str(song_xml.Notes.text)
                 else:

=== modified file 'openlp/plugins/songs/lib/importers/easyslides.py'
--- openlp/plugins/songs/lib/importers/easyslides.py	2016-01-09 13:53:59 +0000
+++ openlp/plugins/songs/lib/importers/easyslides.py	2016-05-27 08:20:16 +0000
@@ -45,7 +45,7 @@
         super(EasySlidesImport, self).__init__(manager, **kwargs)
 
     def do_import(self):
-        log.info('Importing EasySlides XML file %s', self.import_source)
+        log.info('Importing EasySlides XML file {source}'.format(source=self.import_source))
         parser = etree.XMLParser(remove_blank_text=True)
         parsed_file = etree.parse(self.import_source, parser)
         xml = etree.tostring(parsed_file).decode()
@@ -96,10 +96,10 @@
         try:
             setattr(self, self_attribute, str(import_attribute).strip())
         except UnicodeDecodeError:
-            log.exception('UnicodeDecodeError decoding %s' % import_attribute)
+            log.exception('UnicodeDecodeError decoding {attribute}'.format(attribute=import_attribute))
             self._success = False
         except AttributeError:
-            log.exception('No attribute %s' % import_attribute)
+            log.exception('No attribute {attribute}'.format(attribute=import_attribute))
             if mandatory:
                 self._success = False
 
@@ -119,7 +119,7 @@
         try:
             self.add_copyright(str(element).strip())
         except UnicodeDecodeError:
-            log.exception('Unicode error on decoding copyright: %s' % element)
+            log.exception('Unicode error on decoding copyright: {element}'.format(element=element))
             self._success = False
         except AttributeError:
             pass
@@ -157,9 +157,10 @@
         separators = (separator_lines > 0)
         # the number of different regions in song - 1
         if len(region_lines) > 1:
-            log.info('EasySlidesImport: the file contained a song named "%s"'
-                     'with more than two regions, but only two regions are tested, encountered regions were: %s',
-                     self.title, ','.join(list(region_lines.keys())))
+            log.info('EasySlidesImport: the file contained a song named "{title}"'
+                     'with more than two regions, but only two regions are tested, '
+                     'encountered regions were: {keys}'.format(title=self.title,
+                                                               keys=','.join(list(region_lines.keys()))))
         # if the song has regions
         regions = (len(region_lines) > 0)
         # if the regions are inside verses
@@ -232,7 +233,7 @@
         for [reg, vt, vn, inst] in our_verse_order:
             if self._list_has(verses, [reg, vt, vn, inst]):
                 # this is false, but needs user input
-                versetag = '%s%s' % (vt, vn)
+                versetag = '{tag}{number}'.format(tag=vt, number=vn)
                 versetags.append(versetag)
                 lines = '\n'.join(verses[reg][vt][vn][inst])
                 self.add_verse(lines, versetag)
@@ -259,7 +260,8 @@
                 if tag in versetags:
                     self.verse_order_list.append(tag)
                 else:
-                    log.info('Got order item %s, which is not in versetags, dropping item from presentation order', tag)
+                    log.info('Got order item {tag}, which is not in versetags, dropping item from presentation '
+                             'order'.format(tag=tag))
         except UnicodeDecodeError:
             log.exception('Unicode decode error while decoding Sequence')
             self._success = False

=== modified file 'openlp/plugins/songs/lib/importers/easyworship.py'
--- openlp/plugins/songs/lib/importers/easyworship.py	2016-03-12 21:25:39 +0000
+++ openlp/plugins/songs/lib/importers/easyworship.py	2016-05-27 08:20:16 +0000
@@ -171,15 +171,16 @@
                 if copyright:
                     self.copyright += ', '
                 self.copyright += translate('SongsPlugin.EasyWorshipSongImport',
-                                            'Administered by %s') % admin
+                                            'Administered by {admin}').format(admin=admin)
             # Set the SongImport object members.
             self.set_song_import_object(authors, inflated_content)
             if self.stop_import_flag:
                 break
             if self.entry_error_log:
                 self.log_error(self.import_source,
-                               translate('SongsPlugin.EasyWorshipSongImport', '"%s" could not be imported. %s')
-                               % (self.title, self.entry_error_log))
+                               translate('SongsPlugin.EasyWorshipSongImport',
+                                         '"{title}" could not be imported. {entry}').format(title=self.title,
+                                                                                            entry=self.entry_error_log))
                 self.entry_error_log = ''
             elif not self.finish():
                 self.log_error(self.import_source)
@@ -306,7 +307,7 @@
                         if copy:
                             self.copyright += ', '
                         self.copyright += translate('SongsPlugin.EasyWorshipSongImport',
-                                                    'Administered by %s') % admin.decode(self.encoding)
+                                                    'Administered by {admin}').format(admin=admin.decode(self.encoding))
                     if ccli:
                         self.ccli_number = ccli.decode(self.encoding)
                     if authors:
@@ -319,15 +320,17 @@
                         break
                     if self.entry_error_log:
                         self.log_error(self.import_source,
-                                       translate('SongsPlugin.EasyWorshipSongImport', '"%s" could not be imported. %s')
-                                       % (self.title, self.entry_error_log))
+                                       translate('SongsPlugin.EasyWorshipSongImport',
+                                                 '"{title}" could not be imported. '
+                                                 '{entry}').format(title=self.title, entry=self.entry_error_log))
                         self.entry_error_log = ''
                     elif not self.finish():
                         self.log_error(self.import_source)
                 except Exception as e:
                     self.log_error(self.import_source,
-                                   translate('SongsPlugin.EasyWorshipSongImport', '"%s" could not be imported. %s')
-                                   % (self.title, e))
+                                   translate('SongsPlugin.EasyWorshipSongImport',
+                                             '"{title}" could not be imported. {error}').format(title=self.title,
+                                                                                                error=e))
         db_file.close()
         self.memo_file.close()
 
@@ -421,7 +424,7 @@
         fsl = ['>']
         for field_desc in field_descriptions:
             if field_desc.field_type == FieldType.String:
-                fsl.append('%ds' % field_desc.size)
+                fsl.append('{size:d}s'.format(size=field_desc.size))
             elif field_desc.field_type == FieldType.Int16:
                 fsl.append('H')
             elif field_desc.field_type == FieldType.Int32:
@@ -429,13 +432,13 @@
             elif field_desc.field_type == FieldType.Logical:
                 fsl.append('B')
             elif field_desc.field_type == FieldType.Memo:
-                fsl.append('%ds' % field_desc.size)
+                fsl.append('{size:d}s'.format(size=field_desc.size))
             elif field_desc.field_type == FieldType.Blob:
-                fsl.append('%ds' % field_desc.size)
+                fsl.append('{size:d}s'.format(size=field_desc.size))
             elif field_desc.field_type == FieldType.Timestamp:
                 fsl.append('Q')
             else:
-                fsl.append('%ds' % field_desc.size)
+                fsl.append('{size:d}s'.format(size=field_desc.size))
         self.record_structure = struct.Struct(''.join(fsl))
         self.field_descriptions = field_descriptions
 

=== modified file 'openlp/plugins/songs/lib/importers/foilpresenter.py'
--- openlp/plugins/songs/lib/importers/foilpresenter.py	2016-04-22 18:25:57 +0000
+++ openlp/plugins/songs/lib/importers/foilpresenter.py	2016-05-27 08:20:16 +0000
@@ -121,6 +121,7 @@
         for file_path in self.import_source:
             if self.stop_import_flag:
                 return
+            # TODO: Verify format() with template strings
             self.import_wizard.increment_progress_bar(WizardStrings.ImportingType % os.path.basename(file_path))
             try:
                 parsed_file = etree.parse(file_path, parser)
@@ -128,7 +129,7 @@
                 self.foil_presenter.xml_to_song(xml)
             except etree.XMLSyntaxError:
                 self.log_error(file_path, SongStrings.XMLSyntaxError)
-                log.exception('XML syntax error in file %s' % file_path)
+                log.exception('XML syntax error in file {path}'.format(path=file_path))
 
 
 class FoilPresenter(object):

=== modified file 'openlp/plugins/songs/lib/importers/lyrix.py'
--- openlp/plugins/songs/lib/importers/lyrix.py	2015-12-31 22:46:06 +0000
+++ openlp/plugins/songs/lib/importers/lyrix.py	2016-05-27 08:20:16 +0000
@@ -102,8 +102,8 @@
                     else:
                         current_verse += '\n' + line
         except Exception as e:
-            self.log_error(translate('SongsPlugin.LyrixImport', 'File %s' % file.name),
-                           translate('SongsPlugin.LyrixImport', 'Error: %s') % e)
+            self.log_error(translate('SongsPlugin.LyrixImport', 'File {name}').format(name=file.name),
+                           translate('SongsPlugin.LyrixImport', 'Error: {error}').format(error=e))
             return
         self.title = song_title
         self.parse_author(author)

=== modified file 'openlp/plugins/songs/lib/importers/mediashout.py'
--- openlp/plugins/songs/lib/importers/mediashout.py	2015-12-31 22:46:06 +0000
+++ openlp/plugins/songs/lib/importers/mediashout.py	2016-05-27 08:20:16 +0000
@@ -23,6 +23,10 @@
 The :mod:`mediashout` module provides the functionality for importing
 a MediaShout database into the OpenLP database.
 """
+
+# WARNING: See https://docs.python.org/2/library/sqlite3.html for value substitution
+#          in SQL statements
+
 import pyodbc
 
 from openlp.core.lib import translate
@@ -47,8 +51,8 @@
         Receive a single file to import.
         """
         try:
-            conn = pyodbc.connect('DRIVER={Microsoft Access Driver (*.mdb)};DBQ=%s;PWD=6NOZ4eHK7k' %
-                                  self.import_source)
+            conn = pyodbc.connect('DRIVER={Microsoft Access Driver (*.mdb)};DBQ={source};'
+                                  'PWD=6NOZ4eHK7k'.format(sorce=self.import_source))
         except:
             # Unfortunately no specific exception type
             self.log_error(self.import_source, translate('SongsPlugin.MediaShoutImport',
@@ -61,16 +65,15 @@
         for song in songs:
             if self.stop_import_flag:
                 break
-            cursor.execute('SELECT Type, Number, Text FROM Verses WHERE Record = %s ORDER BY Type, Number'
-                           % song.Record)
+            cursor.execute('SELECT Type, Number, Text FROM Verses WHERE Record = ? ORDER BY Type, Number', song.Record)
             verses = cursor.fetchall()
-            cursor.execute('SELECT Type, Number, POrder FROM PlayOrder WHERE Record = %s ORDER BY POrder' % song.Record)
+            cursor.execute('SELECT Type, Number, POrder FROM PlayOrder WHERE Record = ? ORDER BY POrder', song.Record)
             verse_order = cursor.fetchall()
             cursor.execute('SELECT Name FROM Themes INNER JOIN SongThemes ON SongThemes.ThemeId = Themes.ThemeId '
-                           'WHERE SongThemes.Record = %s' % song.Record)
+                           'WHERE SongThemes.Record = ?', song.Record)
             topics = cursor.fetchall()
             cursor.execute('SELECT Name FROM Groups INNER JOIN SongGroups ON SongGroups.GroupId = Groups.GroupId '
-                           'WHERE SongGroups.Record = %s' % song.Record)
+                           'WHERE SongGroups.Record = ?', song.Record)
             topics += cursor.fetchall()
             self.process_song(song, verses, verse_order, topics)
 

=== modified file 'openlp/plugins/songs/lib/importers/openlp.py'
--- openlp/plugins/songs/lib/importers/openlp.py	2016-04-27 18:58:35 +0000
+++ openlp/plugins/songs/lib/importers/openlp.py	2016-05-27 08:20:16 +0000
@@ -102,7 +102,7 @@
             self.log_error(self.import_source, translate('SongsPlugin.OpenLPSongImport',
                                                          'Not a valid OpenLP 2 song database.'))
             return
-        self.import_source = 'sqlite:///%s' % self.import_source
+        self.import_source = 'sqlite:///{url}'.format(url=self.import_source)
         # Load the db file and reflect it
         engine = create_engine(self.import_source)
         source_meta = MetaData()
@@ -239,8 +239,10 @@
             self.manager.save_object(new_song)
             if progress_dialog:
                 progress_dialog.setValue(progress_dialog.value() + 1)
+                # TODO: Verify format() with template strings
                 progress_dialog.setLabelText(WizardStrings.ImportingType % new_song.title)
             else:
+                # TODO: Verify format() with template strings
                 self.import_wizard.increment_progress_bar(WizardStrings.ImportingType % new_song.title)
             if self.stop_import_flag:
                 break

=== modified file 'openlp/plugins/songs/lib/importers/openlyrics.py'
--- openlp/plugins/songs/lib/importers/openlyrics.py	2016-04-22 18:25:57 +0000
+++ openlp/plugins/songs/lib/importers/openlyrics.py	2016-05-27 08:20:16 +0000
@@ -58,6 +58,7 @@
         for file_path in self.import_source:
             if self.stop_import_flag:
                 return
+            # TODO: Verify format() with template strings
             self.import_wizard.increment_progress_bar(WizardStrings.ImportingType % os.path.basename(file_path))
             try:
                 # Pass a file object, because lxml does not cope with some
@@ -66,9 +67,10 @@
                 xml = etree.tostring(parsed_file).decode()
                 self.open_lyrics.xml_to_song(xml)
             except etree.XMLSyntaxError:
-                log.exception('XML syntax error in file %s' % file_path)
+                log.exception('XML syntax error in file {path}'.format(file_path))
                 self.log_error(file_path, SongStrings.XMLSyntaxError)
             except OpenLyricsError as exception:
-                log.exception('OpenLyricsException %d in file %s: %s' %
-                              (exception.type, file_path, exception.log_message))
+                log.exception('OpenLyricsException {error:d} in file {name}: {text}'.format(error=exception.type,
+                                                                                            name=file_path,
+                                                                                            text=exception.log_message))
                 self.log_error(file_path, exception.display_message)

=== modified file 'openlp/plugins/songs/lib/importers/openoffice.py'
--- openlp/plugins/songs/lib/importers/openoffice.py	2016-04-05 17:10:51 +0000
+++ openlp/plugins/songs/lib/importers/openoffice.py	2016-05-27 08:20:16 +0000
@@ -161,7 +161,7 @@
             else:
                 self.import_wizard.increment_progress_bar('Processing file ' + file_path, 0)
         except AttributeError:
-            log.exception("open_ooo_file failed: %s", url)
+            log.exception("open_ooo_file failed: {url}".format(url=url))
         return
 
     def create_property(self, name, value):

=== modified file 'openlp/plugins/songs/lib/importers/opensong.py'
--- openlp/plugins/songs/lib/importers/opensong.py	2016-01-23 08:19:12 +0000
+++ openlp/plugins/songs/lib/importers/opensong.py	2016-05-27 08:20:16 +0000
@@ -254,8 +254,8 @@
             length = 0
             while length < len(verse_num) and verse_num[length].isnumeric():
                 length += 1
-            verse_def = '%s%s' % (verse_tag, verse_num[:length])
-            verse_joints[verse_def] = '%s\n[---]\n%s' % (verse_joints[verse_def], lines) \
+            verse_def = '{tag}{number}'.format(tag=verse_tag, number=verse_num[:length])
+            verse_joints[verse_def] = '{verse}\n[---]\n{lines}'.format(verse=verse_joints[verse_def], lines=lines) \
                 if verse_def in verse_joints else lines
         # Parsing the dictionary produces the elements in a non-intuitive order.  While it "works", it's not a
         # natural layout should the user come back to edit the song.  Instead we sort by the verse type, so that we
@@ -287,11 +287,11 @@
                     verse_num = '1'
                 verse_index = VerseType.from_loose_input(verse_tag)
                 verse_tag = VerseType.tags[verse_index]
-                verse_def = '%s%s' % (verse_tag, verse_num)
+                verse_def = '{tag}{number}'.format(tag=verse_tag, number=verse_num)
                 if verse_num in verses.get(verse_tag, {}):
                     self.verse_order_list.append(verse_def)
                 else:
-                    log.info('Got order %s but not in verse tags, dropping this item from presentation order',
-                             verse_def)
+                    log.info('Got order {order} but not in verse tags, dropping this item from presentation '
+                             'order'.format(order=verse_def))
         if not self.finish():
             self.log_error(file.name)

=== modified file 'openlp/plugins/songs/lib/importers/opspro.py'
--- openlp/plugins/songs/lib/importers/opspro.py	2016-03-20 20:23:01 +0000
+++ openlp/plugins/songs/lib/importers/opspro.py	2016-05-27 08:20:16 +0000
@@ -23,6 +23,10 @@
 The :mod:`opspro` module provides the functionality for importing
 a OPS Pro database into the OpenLP database.
 """
+
+# WARNING: See https://docs.python.org/2/library/sqlite3.html for value substitution
+#          in SQL statements
+
 import logging
 import re
 import pyodbc
@@ -51,10 +55,11 @@
         """
         password = self.extract_mdb_password()
         try:
-            conn = pyodbc.connect('DRIVER={Microsoft Access Driver (*.mdb)};DBQ=%s;PWD=%s' % (self.import_source,
-                                                                                              password))
+            conn = pyodbc.connect('DRIVER={Microsoft Access Driver (*.mdb)};DBQ={source};'
+                                  'PWD={password}'.format(source=self.import_source, password=password))
         except (pyodbc.DatabaseError, pyodbc.IntegrityError, pyodbc.InternalError, pyodbc.OperationalError) as e:
-            log.warning('Unable to connect the OPS Pro database %s. %s', self.import_source, str(e))
+            log.warning('Unable to connect the OPS Pro database {source}. {error}'.format(source=self.import_source,
+                                                                                          error=str(e)))
             # Unfortunately no specific exception type
             self.log_error(self.import_source, translate('SongsPlugin.OPSProImport',
                                                          'Unable to connect the OPS Pro database.'))
@@ -68,19 +73,19 @@
             if self.stop_import_flag:
                 break
             # Type means: 0=Original, 1=Projection, 2=Own
-            cursor.execute('SELECT Lyrics, Type, IsDualLanguage FROM Lyrics WHERE SongID = %d AND Type < 2 '
-                           'ORDER BY Type DESC' % song.ID)
+            cursor.execute('SELECT Lyrics, Type, IsDualLanguage FROM Lyrics WHERE SongID = ? AND Type < 2 '
+                           'ORDER BY Type DESC', song.ID)
             lyrics = cursor.fetchone()
             cursor.execute('SELECT CategoryName FROM Category INNER JOIN SongCategory '
-                           'ON Category.ID = SongCategory.CategoryID WHERE SongCategory.SongID = %d '
-                           'ORDER BY CategoryName' % song.ID)
+                           'ON Category.ID = SongCategory.CategoryID WHERE SongCategory.SongID = ? '
+                           'ORDER BY CategoryName', song.ID)
             topics = cursor.fetchall()
             try:
                 self.process_song(song, lyrics, topics)
             except Exception as e:
                 self.log_error(self.import_source,
-                               translate('SongsPlugin.OPSProImport', '"%s" could not be imported. %s')
-                               % (song.Title, e))
+                               translate('SongsPlugin.OPSProImport',
+                                         '"{title}" could not be imported. {error}').format(title=song.Title, error=e))
 
     def process_song(self, song, lyrics, topics):
         """

=== modified file 'openlp/plugins/songs/lib/importers/powerpraise.py'
--- openlp/plugins/songs/lib/importers/powerpraise.py	2016-04-22 18:25:57 +0000
+++ openlp/plugins/songs/lib/importers/powerpraise.py	2016-05-27 08:20:16 +0000
@@ -41,6 +41,7 @@
         for file_path in self.import_source:
             if self.stop_import_flag:
                 return
+            # TODO: Verify format() with template strings
             self.import_wizard.increment_progress_bar(WizardStrings.ImportingType % os.path.basename(file_path))
             root = objectify.parse(open(file_path, 'rb')).getroot()
             self.process_song(root)
@@ -66,7 +67,7 @@
             else:
                 verse_def = 'o'
             verse_count[verse_def] = verse_count.get(verse_def, 0) + 1
-            verse_def = '%s%d' % (verse_def, verse_count[verse_def])
+            verse_def = '{verse}{count:d}'.format(verse=verse_def, count=verse_count[verse_def])
             verse_text = []
             for slide in part.slide:
                 if not hasattr(slide, 'line'):

=== modified file 'openlp/plugins/songs/lib/importers/powersong.py'
--- openlp/plugins/songs/lib/importers/powersong.py	2015-12-31 22:46:06 +0000
+++ openlp/plugins/songs/lib/importers/powersong.py	2016-05-27 08:20:16 +0000
@@ -96,7 +96,7 @@
                 self.import_source = ''
         if not self.import_source or not isinstance(self.import_source, list):
             self.log_error(translate('SongsPlugin.PowerSongImport', 'No songs to import.'),
-                           translate('SongsPlugin.PowerSongImport', 'No %s files found.') % ps_string)
+                           translate('SongsPlugin.PowerSongImport', 'No {text} files found.').format(text=ps_string))
             return
         self.import_wizard.progress_bar.setMaximum(len(self.import_source))
         for file in self.import_source:
@@ -113,9 +113,9 @@
                         field = self._read_string(song_data)
                     except ValueError:
                         parse_error = True
-                        self.log_error(os.path.basename(file), str(
-                            translate('SongsPlugin.PowerSongImport', 'Invalid %s file. Unexpected byte value.')) %
-                            ps_string)
+                        self.log_error(os.path.basename(file),
+                                       translate('SongsPlugin.PowerSongImport',
+                                                 'Invalid {text} file. Unexpected byte value.').format(text=ps_string))
                         break
                     else:
                         if label == 'TITLE':
@@ -131,19 +131,20 @@
                 continue
             # Check that file had TITLE field
             if not self.title:
-                self.log_error(os.path.basename(file), str(
-                    translate('SongsPlugin.PowerSongImport', 'Invalid %s file. Missing "TITLE" header.')) % ps_string)
+                self.log_error(os.path.basename(file),
+                               translate('SongsPlugin.PowerSongImport',
+                                         'Invalid {text} file. Missing "TITLE" header.').format(text=ps_string))
                 continue
             # Check that file had COPYRIGHTLINE label
             if not found_copyright:
-                self.log_error(self.title, str(
-                    translate('SongsPlugin.PowerSongImport', 'Invalid %s file. Missing "COPYRIGHTLINE" header.')) %
-                    ps_string)
+                self.log_error(self.title,
+                               translate('SongsPlugin.PowerSongImport',
+                                         'Invalid {text} file. Missing "COPYRIGHTLINE" header.').format(text=ps_string))
                 continue
             # Check that file had at least one verse
             if not self.verses:
-                self.log_error(self.title, str(
-                    translate('SongsPlugin.PowerSongImport', 'Verses not found. Missing "PART" header.')))
+                self.log_error(self.title,
+                               translate('SongsPlugin.PowerSongImport', 'Verses not found. Missing "PART" header.'))
                 continue
             if not self.finish():
                 self.log_error(self.title)

=== modified file 'openlp/plugins/songs/lib/importers/presentationmanager.py'
--- openlp/plugins/songs/lib/importers/presentationmanager.py	2016-04-22 18:25:57 +0000
+++ openlp/plugins/songs/lib/importers/presentationmanager.py	2016-05-27 08:20:16 +0000
@@ -44,6 +44,7 @@
         for file_path in self.import_source:
             if self.stop_import_flag:
                 return
+            # TODO: Verify format() with template strings
             self.import_wizard.increment_progress_bar(WizardStrings.ImportingType % os.path.basename(file_path))
             try:
                 tree = etree.parse(file_path, parser=etree.XMLParser(recover=True))
@@ -90,7 +91,7 @@
                 verse_def = 'o'
             if not is_duplicate:  # Only increment verse number if no duplicate
                 verse_count[verse_def] = verse_count.get(verse_def, 0) + 1
-            verse_def = '%s%d' % (verse_def, verse_count[verse_def])
+            verse_def = '{verse}{count:d}'.format(verse=verse_def, count=verse_count[verse_def])
             if not is_duplicate:  # Only add verse if no duplicate
                 self.add_verse(str(verse).strip(), verse_def)
             verse_order_list.append(verse_def)

=== modified file 'openlp/plugins/songs/lib/importers/propresenter.py'
--- openlp/plugins/songs/lib/importers/propresenter.py	2016-04-22 18:25:57 +0000
+++ openlp/plugins/songs/lib/importers/propresenter.py	2016-05-27 08:20:16 +0000
@@ -46,6 +46,7 @@
         for file_path in self.import_source:
             if self.stop_import_flag:
                 return
+            # TODO: Verify format() with template strings
             self.import_wizard.increment_progress_bar(WizardStrings.ImportingType % os.path.basename(file_path))
             root = objectify.parse(open(file_path, 'rb')).getroot()
             self.process_song(root, file_path)
@@ -87,7 +88,7 @@
                 RTFData = slide.displayElements.RVTextElement.get('RTFData')
                 rtf = base64.standard_b64decode(RTFData)
                 words, encoding = strip_rtf(rtf.decode())
-                self.add_verse(words, "v%d" % count)
+                self.add_verse(words, "v{count}".format(count=count))
 
         # ProPresenter 5
         elif(self.version >= 500 and self.version < 600):
@@ -103,7 +104,7 @@
                     RTFData = slide.displayElements.RVTextElement.get('RTFData')
                     rtf = base64.standard_b64decode(RTFData)
                     words, encoding = strip_rtf(rtf.decode())
-                    self.add_verse(words, "v%d" % count)
+                    self.add_verse(words, "v{count:d}".format(count=count))
 
         # ProPresenter 6
         elif(self.version >= 600 and self.version < 700):
@@ -127,7 +128,7 @@
                                 words, encoding = strip_rtf(data.decode())
                                 break
                         if words:
-                            self.add_verse(words, "v%d" % count)
+                            self.add_verse(words, "v{count:d}".format(count=count))
 
         if not self.finish():
             self.log_error(self.import_source)

=== modified file 'openlp/plugins/songs/lib/importers/songimport.py'
--- openlp/plugins/songs/lib/importers/songimport.py	2016-04-22 18:25:57 +0000
+++ openlp/plugins/songs/lib/importers/songimport.py	2016-05-27 08:20:16 +0000
@@ -117,7 +117,7 @@
             self.import_wizard.error_report_text_edit.setVisible(True)
             self.import_wizard.error_copy_to_button.setVisible(True)
             self.import_wizard.error_save_to_button.setVisible(True)
-        self.import_wizard.error_report_text_edit.append('- %s (%s)' % (file_path, reason))
+        self.import_wizard.error_report_text_edit.append('- {path} ({error})'.format(path=file_path, error=reason))
 
     def stop_import(self):
         """
@@ -326,10 +326,11 @@
         if not self.check_complete():
             self.set_defaults()
             return False
-        log.info('committing song %s to database', self.title)
+        log.info('committing song {title} to database'.format(title=self.title))
         song = Song()
         song.title = self.title
         if self.import_wizard is not None:
+            # TODO: Verify format() with template variables
             self.import_wizard.increment_progress_bar(WizardStrings.ImportingType % song.title)
         song.alternate_title = self.alternate_title
         # Values will be set when cleaning the song.
@@ -344,11 +345,11 @@
             if verse_def[0].lower() in VerseType.tags:
                 verse_tag = verse_def[0].lower()
             else:
-                new_verse_def = '%s%d' % (VerseType.tags[VerseType.Other], other_count)
+                new_verse_def = '{tag}{count:d}'.format(tag=VerseType.tags[VerseType.Other], count=other_count)
                 verses_changed_to_other[verse_def] = new_verse_def
                 other_count += 1
                 verse_tag = VerseType.tags[VerseType.Other]
-                log.info('Versetype %s changing to %s', verse_def, new_verse_def)
+                log.info('Versetype {old} changing to {new}'.format(old=verse_def, new=new_verse_def))
                 verse_def = new_verse_def
             sxml.add_verse_to_lyrics(verse_tag, verse_def[1:], verse_text, lang)
         song.lyrics = str(sxml.extract_xml(), 'utf-8')

=== modified file 'openlp/plugins/songs/lib/importers/songshowplus.py'
--- openlp/plugins/songs/lib/importers/songshowplus.py	2016-04-22 18:25:57 +0000
+++ openlp/plugins/songs/lib/importers/songshowplus.py	2016-05-27 08:20:16 +0000
@@ -101,6 +101,7 @@
             self.other_count = 0
             self.other_list = {}
             file_name = os.path.split(file)[1]
+            # TODO: Verify format() with template variables
             self.import_wizard.increment_progress_bar(WizardStrings.ImportingType % file_name, 0)
             song_data = open(file, 'rb')
             while True:
@@ -145,13 +146,16 @@
                     if match:
                         self.ccli_number = int(match.group())
                     else:
-                        log.warning("Can't parse CCLI Number from string: %s" % self.decode(data))
+                        log.warning("Can't parse CCLI Number from string: {text}".format(text=self.decode(data)))
                 elif block_key == VERSE:
-                    self.add_verse(self.decode(data), "%s%s" % (VerseType.tags[VerseType.Verse], verse_no))
+                    self.add_verse(self.decode(data), "{tag}{number}".format(tag=VerseType.tags[VerseType.Verse],
+                                                                             number=verse_no))
                 elif block_key == CHORUS:
-                    self.add_verse(self.decode(data), "%s%s" % (VerseType.tags[VerseType.Chorus], verse_no))
+                    self.add_verse(self.decode(data), "{tag}{number}".format(tag=VerseType.tags[VerseType.Chorus],
+                                                                             number=verse_no))
                 elif block_key == BRIDGE:
-                    self.add_verse(self.decode(data), "%s%s" % (VerseType.tags[VerseType.Bridge], verse_no))
+                    self.add_verse(self.decode(data), "{tag}{number}".format(tag=VerseType.tags[VerseType.Bridge],
+                                                                             number=verse_no))
                 elif block_key == TOPIC:
                     self.topics.append(self.decode(data))
                 elif block_key == COMMENTS:
@@ -170,7 +174,7 @@
                     verse_tag = self.to_openlp_verse_tag(verse_name)
                     self.add_verse(self.decode(data), verse_tag)
                 else:
-                    log.debug("Unrecognised blockKey: %s, data: %s" % (block_key, data))
+                    log.debug("Unrecognised blockKey: {key}, data: {data}".format(key=block_key, data=data))
                     song_data.seek(next_block_starts)
             self.verse_order_list = self.ssp_verse_order_list
             song_data.close()

=== modified file 'openlp/plugins/songs/lib/importers/sundayplus.py'
--- openlp/plugins/songs/lib/importers/sundayplus.py	2016-01-09 09:09:29 +0000
+++ openlp/plugins/songs/lib/importers/sundayplus.py	2016-05-27 08:20:16 +0000
@@ -141,7 +141,7 @@
                         if len(value):
                             verse_type = VerseType.tags[VerseType.from_loose_input(value[0])]
                             if len(value) >= 2 and value[-1] in ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']:
-                                verse_type = "%s%s" % (verse_type, value[-1])
+                                verse_type = "{verse}{value}".format(verse=verse_type, value=value[-1])
                     elif name == 'HOTKEY':
                         value = self.decode(value).strip()
                         # HOTKEY always appears after MARKER_NAME, so it

=== modified file 'openlp/plugins/songs/lib/importers/videopsalm.py'
--- openlp/plugins/songs/lib/importers/videopsalm.py	2016-01-08 19:52:24 +0000
+++ openlp/plugins/songs/lib/importers/videopsalm.py	2016-05-27 08:20:16 +0000
@@ -115,8 +115,8 @@
                 for verse in song['Verses']:
                     self.add_verse(verse['Text'], 'v')
                 if not self.finish():
-                    self.log_error('Could not import %s' % self.title)
+                    self.log_error('Could not import {title}'.format(title=self.title))
         except Exception as e:
-            self.log_error(translate('SongsPlugin.VideoPsalmImport', 'File %s' % file.name),
-                           translate('SongsPlugin.VideoPsalmImport', 'Error: %s') % e)
+            self.log_error(translate('SongsPlugin.VideoPsalmImport', 'File {name}').format(name=file.name),
+                           translate('SongsPlugin.VideoPsalmImport', 'Error: {error}').format(error=e))
         song_file.close()

=== modified file 'openlp/plugins/songs/lib/importers/wordsofworship.py'
--- openlp/plugins/songs/lib/importers/wordsofworship.py	2016-03-29 20:45:57 +0000
+++ openlp/plugins/songs/lib/importers/wordsofworship.py	2016-05-27 08:20:16 +0000
@@ -108,8 +108,8 @@
                 if song_data.read(19).decode() != 'WoW File\nSong Words':
                     self.log_error(source,
                                    translate('SongsPlugin.WordsofWorshipSongImport',
-                                             'Invalid Words of Worship song file. Missing "%s" header.')
-                                   % 'WoW File\\nSong Words')
+                                             'Invalid Words of Worship song file. Missing "{text}" '
+                                             'header.').format(text='WoW File\\nSong Words'))
                     continue
                 # Seek to byte which stores number of blocks in the song
                 song_data.seek(56)
@@ -118,8 +118,8 @@
                 if song_data.read(16).decode() != 'CSongDoc::CBlock':
                     self.log_error(source,
                                    translate('SongsPlugin.WordsofWorshipSongImport',
-                                             'Invalid Words of Worship song file. Missing "%s" string.')
-                                   % 'CSongDoc::CBlock')
+                                             'Invalid Words of Worship song file. Missing "{text}" '
+                                             'string.').format(text='CSongDoc::CBlock'))
                     continue
                 # Seek to the beginning of the first block
                 song_data.seek(82)

=== modified file 'openlp/plugins/songs/lib/importers/worshipassistant.py'
--- openlp/plugins/songs/lib/importers/worshipassistant.py	2015-12-31 22:46:06 +0000
+++ openlp/plugins/songs/lib/importers/worshipassistant.py	2016-05-27 08:20:16 +0000
@@ -91,11 +91,11 @@
             records = list(songs_reader)
         except csv.Error as e:
             self.log_error(translate('SongsPlugin.WorshipAssistantImport', 'Error reading CSV file.'),
-                           translate('SongsPlugin.WorshipAssistantImport', 'Line %d: %s') %
-                           (songs_reader.line_num, e))
+                           translate('SongsPlugin.WorshipAssistantImport',
+                                     'Line {number:d}: {error}').format(number=songs_reader.line_num, error=e))
             return
         num_records = len(records)
-        log.info('%s records found in CSV file' % num_records)
+        log.info('{count} records found in CSV file'.format(count=num_records))
         self.import_wizard.progress_bar.setMaximum(num_records)
         # Create regex to strip html tags
         re_html_strip = re.compile(r'<[^>]+>')
@@ -122,12 +122,14 @@
                     verse_order_list = [x.strip() for x in record['ROADMAP'].split(',')]
                 lyrics = record['LYRICS2']
             except UnicodeDecodeError as e:
-                self.log_error(translate('SongsPlugin.WorshipAssistantImport', 'Record %d' % index),
-                               translate('SongsPlugin.WorshipAssistantImport', 'Decoding error: %s') % e)
+                self.log_error(translate('SongsPlugin.WorshipAssistantImport', 'Record {count:d}').format(count=index),
+                               translate('SongsPlugin.WorshipAssistantImport',
+                                         'Decoding error: {error}').format(error=e))
                 continue
             except TypeError as e:
                 self.log_error(translate('SongsPlugin.WorshipAssistantImport',
-                                         'File not valid WorshipAssistant CSV format.'), 'TypeError: %s' % e)
+                                         'File not valid WorshipAssistant CSV format.'),
+                               'TypeError: {error}'.format(error=e))
                 return
             verse = ''
             used_verses = []
@@ -180,6 +182,7 @@
                         cleaned_verse_order_list.append(verse)
                 self.verse_order_list = cleaned_verse_order_list
             if not self.finish():
-                self.log_error(translate('SongsPlugin.WorshipAssistantImport', 'Record %d') % index +
+                self.log_error(translate('SongsPlugin.WorshipAssistantImport',
+                                         'Record {count:d}').format(count=index) +
                                (': "' + self.title + '"' if self.title else ''))
             songs_file.close()

=== modified file 'openlp/plugins/songs/lib/importers/worshipcenterpro.py'
--- openlp/plugins/songs/lib/importers/worshipcenterpro.py	2015-12-31 22:46:06 +0000
+++ openlp/plugins/songs/lib/importers/worshipcenterpro.py	2016-05-27 08:20:16 +0000
@@ -49,9 +49,11 @@
         Receive a single file to import.
         """
         try:
-            conn = pyodbc.connect('DRIVER={Microsoft Access Driver (*.mdb)};DBQ=%s' % self.import_source)
+            conn = pyodbc.connect('DRIVER={Microsoft Access Driver (*.mdb)};'
+                                  'DBQ={source}'.format(source=self.import_source))
         except (pyodbc.DatabaseError, pyodbc.IntegrityError, pyodbc.InternalError, pyodbc.OperationalError) as e:
-            log.warning('Unable to connect the WorshipCenter Pro database %s. %s', self.import_source, str(e))
+            log.warning('Unable to connect the WorshipCenter Pro '
+                        'database {source}. {error}'.format(source=self.import_source, error=str(e)))
             # Unfortunately no specific exception type
             self.log_error(self.import_source, translate('SongsPlugin.WorshipCenterProImport',
                                                          'Unable to connect the WorshipCenter Pro database.'))

=== modified file 'openlp/plugins/songs/lib/importers/zionworx.py'
--- openlp/plugins/songs/lib/importers/zionworx.py	2015-12-31 22:46:06 +0000
+++ openlp/plugins/songs/lib/importers/zionworx.py	2016-05-27 08:20:16 +0000
@@ -84,10 +84,11 @@
                 records = list(songs_reader)
             except csv.Error as e:
                 self.log_error(translate('SongsPlugin.ZionWorxImport', 'Error reading CSV file.'),
-                               translate('SongsPlugin.ZionWorxImport', 'Line %d: %s') % (songs_reader.line_num, e))
+                               translate('SongsPlugin.ZionWorxImport',
+                                         'Line {number:d}: {error}').format(number=songs_reader.line_num, error=e))
                 return
             num_records = len(records)
-            log.info('%s records found in CSV file' % num_records)
+            log.info('{count} records found in CSV file'.format(count=num_records))
             self.import_wizard.progress_bar.setMaximum(num_records)
             for index, record in enumerate(records, 1):
                 if self.stop_import_flag:
@@ -101,12 +102,12 @@
                     self.add_copyright(self._decode(record['Copyright']))
                     lyrics = self._decode(record['Lyrics'])
                 except UnicodeDecodeError as e:
-                    self.log_error(translate('SongsPlugin.ZionWorxImport', 'Record %d' % index),
-                                   translate('SongsPlugin.ZionWorxImport', 'Decoding error: %s') % e)
+                    self.log_error(translate('SongsPlugin.ZionWorxImport', 'Record {index}').format(index=index),
+                                   translate('SongsPlugin.ZionWorxImport', 'Decoding error: {error}').format(error=e))
                     continue
                 except TypeError as e:
-                    self.log_error(translate(
-                        'SongsPlugin.ZionWorxImport', 'File not valid ZionWorx CSV format.'), 'TypeError: %s' % e)
+                    self.log_error(translate('SongsPlugin.ZionWorxImport', 'File not valid ZionWorx CSV format.'),
+                                   'TypeError: {error}'.format(error=e))
                     return
                 verse = ''
                 for line in lyrics.splitlines():

=== modified file 'openlp/plugins/songs/lib/mediaitem.py'
--- openlp/plugins/songs/lib/mediaitem.py	2016-04-29 16:44:24 +0000
+++ openlp/plugins/songs/lib/mediaitem.py	2016-05-27 08:20:16 +0000
@@ -129,7 +129,7 @@
         self.display_copyright_symbol = Settings().value(self.settings_section + '/display copyright symbol')
 
     def retranslateUi(self):
-        self.search_text_label.setText('%s:' % UiStrings().Search)
+        self.search_text_label.setText('{text}:'.format(text=UiStrings().Search))
         self.search_text_button.setText(UiStrings().Search)
         self.maintenance_action.setText(SongStrings.SongMaintenance)
         self.maintenance_action.setToolTip(translate('SongsPlugin.MediaItem',
@@ -166,12 +166,14 @@
                 translate('SongsPlugin.MediaItem', 'CCLI number'),
                 translate('SongsPlugin.MediaItem', 'Search CCLI number...'))
         ])
-        self.search_text_edit.set_current_search_type(Settings().value('%s/last search type' % self.settings_section))
+        self.search_text_edit.set_current_search_type(
+            Settings().value('{section}/last search type'.format(section=self.settings_section)))
         self.config_update()
 
     def on_search_text_button_clicked(self):
         # Save the current search type to the configuration.
-        Settings().setValue('%s/last search type' % self.settings_section, self.search_text_edit.current_search_type())
+        Settings().setValue('{section}/last search type'.format(section=self.settings_section),
+                            self.search_text_edit.current_search_type())
         # Reload the list considering the new search type.
         search_keywords = str(self.search_text_edit.displayText())
         search_type = self.search_text_edit.current_search_type()
@@ -181,31 +183,31 @@
             self.display_results_song(search_results)
         elif search_type == SongSearch.Titles:
             log.debug('Titles Search')
-            search_string = '%' + clean_string(search_keywords) + '%'
+            search_string = '%{text}%'.format(text=clean_string(search_keywords))
             search_results = self.plugin.manager.get_all_objects(Song, Song.search_title.like(search_string))
             self.display_results_song(search_results)
         elif search_type == SongSearch.Lyrics:
             log.debug('Lyrics Search')
-            search_string = '%' + clean_string(search_keywords) + '%'
+            search_string = '%{text}%'.format(text=clean_string(search_keywords))
             search_results = self.plugin.manager.get_all_objects(Song, Song.search_lyrics.like(search_string))
             self.display_results_song(search_results)
         elif search_type == SongSearch.Authors:
             log.debug('Authors Search')
-            search_string = '%' + search_keywords + '%'
+            search_string = '%{text}%'.format(text=search_keywords)
             search_results = self.plugin.manager.get_all_objects(
                 Author, Author.display_name.like(search_string))
             self.display_results_author(search_results)
         elif search_type == SongSearch.Topics:
             log.debug('Topics Search')
-            search_string = '%' + search_keywords + '%'
+            search_string = '%{text}%'.format(text=search_keywords)
             search_results = self.plugin.manager.get_all_objects(
                 Topic, Topic.name.like(search_string))
             self.display_results_topic(search_results)
         elif search_type == SongSearch.Books:
             log.debug('Songbook Search')
             search_keywords = search_keywords.rpartition(' ')
-            search_book = search_keywords[0] + '%'
-            search_entry = search_keywords[2] + '%'
+            search_book = '{text}%'.format(text=search_keywords[0])
+            search_entry = '{text}%'.format(text=search_keywords[2])
             search_results = (self.plugin.manager.session.query(SongBookEntry.entry, Book.name, Song.title, Song.id)
                               .join(Song)
                               .join(Book)
@@ -214,26 +216,26 @@
             self.display_results_book(search_results)
         elif search_type == SongSearch.Themes:
             log.debug('Theme Search')
-            search_string = '%' + search_keywords + '%'
+            search_string = '%{text}%'.format(text=search_keywords)
             search_results = self.plugin.manager.get_all_objects(
                 Song, Song.theme_name.like(search_string))
             self.display_results_themes(search_results)
         elif search_type == SongSearch.Copyright:
             log.debug('Copyright Search')
-            search_string = '%' + search_keywords + '%'
+            search_string = '%{text}%'.format(text=search_keywords)
             search_results = self.plugin.manager.get_all_objects(
                 Song, and_(Song.copyright.like(search_string), Song.copyright != ''))
             self.display_results_song(search_results)
         elif search_type == SongSearch.CCLInumber:
             log.debug('CCLI number Search')
-            search_string = '%' + search_keywords + '%'
+            search_string = '%{text}%'.format(text=search_keywords)
             search_results = self.plugin.manager.get_all_objects(
                 Song, and_(Song.ccli_number.like(search_string), Song.ccli_number != ''))
             self.display_results_cclinumber(search_results)
         self.check_search_result()
 
     def search_entire(self, search_keywords):
-        search_string = '%' + clean_string(search_keywords) + '%'
+        search_string = '%{text}%'.format(text=clean_string(search_keywords))
         return self.plugin.manager.get_all_objects(
             Song, or_(Song.search_title.like(search_string), Song.search_lyrics.like(search_string),
                       Song.comments.like(search_string)))
@@ -272,7 +274,8 @@
             if song.temporary:
                 continue
             author_list = [author.display_name for author in song.authors]
-            song_detail = '%s (%s)' % (song.title, create_separated_list(author_list)) if author_list else song.title
+            text = create_separated_list(author_list) if author_list else song.title
+            song_detail = '{title} ({author})'.format(title=song.title, author=text)
             song_name = QtWidgets.QListWidgetItem(song_detail)
             song_name.setData(QtCore.Qt.UserRole, song.id)
             self.list_view.addItem(song_name)
@@ -305,7 +308,7 @@
                 # Do not display temporary songs
                 if song.temporary:
                     continue
-                song_detail = '%s (%s)' % (author.display_name, song.title)
+                song_detail = '{author} ({title})'.format(author=author.display_name, title=song.title)
                 song_name = QtWidgets.QListWidgetItem(song_detail)
                 song_name.setData(QtCore.Qt.UserRole, song.id)
                 self.list_view.addItem(song_name)
@@ -325,7 +328,8 @@
         self.list_view.clear()
         search_results.sort(key=get_songbook_key)
         for result in search_results:
-            song_detail = '%s #%s: %s' % (result[1], result[0], result[2])
+            song_detail = '{result1} #{result0}: {result2}'.format(result1=result[1], result0=result[0],
+                                                                   result2=result[2])
             song_name = QtWidgets.QListWidgetItem(song_detail)
             song_name.setData(QtCore.Qt.UserRole, result[3])
             self.list_view.addItem(song_name)
@@ -354,7 +358,7 @@
                 # Do not display temporary songs
                 if song.temporary:
                     continue
-                song_detail = '%s (%s)' % (topic.name, song.title)
+                song_detail = '{topic} ({title})'.format(topic=topic.name, title=song.title)
                 song_name = QtWidgets.QListWidgetItem(song_detail)
                 song_name.setData(QtCore.Qt.UserRole, song.id)
                 self.list_view.addItem(song_name)
@@ -377,7 +381,7 @@
             # Do not display temporary songs
             if song.temporary:
                 continue
-            song_detail = '%s (%s)' % (song.theme_name, song.title)
+            song_detail = '{theme} ({song})'.format(theme=song.theme_name, song=song.title)
             song_name = QtWidgets.QListWidgetItem(song_detail)
             song_name.setData(QtCore.Qt.UserRole, song.id)
             self.list_view.addItem(song_name)
@@ -400,7 +404,7 @@
             # Do not display temporary songs
             if song.temporary:
                 continue
-            song_detail = '%s (%s)' % (song.ccli_number, song.title)
+            song_detail = '{ccli} ({song})'.format(ccli=song.ccli_number, song=song.title)
             song_name = QtWidgets.QListWidgetItem(song_detail)
             song_name.setData(QtCore.Qt.UserRole, song.id)
             self.list_view.addItem(song_name)
@@ -456,7 +460,7 @@
         Called by ServiceManager or SlideController by event passing the Song Id in the payload along with an indicator
         to say which type of display is required.
         """
-        log.debug('on_remote_edit for song %s' % song_id)
+        log.debug('on_remote_edit for song {song}'.format(song=song_id))
         song_id = int(song_id)
         valid = self.plugin.manager.get_object(Song, song_id)
         if valid:
@@ -499,7 +503,8 @@
             if QtWidgets.QMessageBox.question(
                     self, UiStrings().ConfirmDelete,
                     translate('SongsPlugin.MediaItem',
-                              'Are you sure you want to delete the "%d" selected song(s)?') % len(items),
+                              'Are you sure you want to delete the "{items:d}" '
+                              'selected song(s)?').format(items=len(items)),
                     QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No),
                     QtWidgets.QMessageBox.Yes) == QtWidgets.QMessageBox.No:
                 return
@@ -524,8 +529,9 @@
             old_song = self.plugin.manager.get_object(Song, item_id)
             song_xml = self.open_lyrics.song_to_xml(old_song)
             new_song = self.open_lyrics.xml_to_song(song_xml)
-            new_song.title = '%s <%s>' % \
-                             (new_song.title, translate('SongsPlugin.MediaItem', 'copy', 'For song cloning'))
+            new_song.title = '{title} <{text}>'.format(title=new_song.title,
+                                                       text=translate('SongsPlugin.MediaItem',
+                                                                      'copy', 'For song cloning'))
             # Copy audio files from the old to the new song
             if len(old_song.media_files) > 0:
                 save_path = os.path.join(AppLocation.get_section_data_path(self.plugin.name), 'audio', str(new_song.id))
@@ -552,7 +558,8 @@
         :param remote: Triggered from remote
         :param context: Why is it being generated
         """
-        log.debug('generate_slide_data: %s, %s, %s' % (service_item, item, self.remote_song))
+        log.debug('generate_slide_data: {service}, {item}, {remote}'.format(service=service_item, item=item,
+                                                                            remote=self.remote_song))
         item_id = self._get_id_of_item_to_generate(item, self.remote_song)
         service_item.add_capability(ItemCapabilities.CanEdit)
         service_item.add_capability(ItemCapabilities.CanPreview)
@@ -581,7 +588,7 @@
                 if verse_index is None:
                     verse_index = VerseType.from_tag(verse_tag)
                 verse_tag = VerseType.translated_tags[verse_index].upper()
-                verse_def = '%s%s' % (verse_tag, verse[0]['label'])
+                verse_def = '{tag}{label}'.format(tag=verse_tag, label=verse[0]['label'])
                 service_item.add_from_text(str(verse[1]), verse_def)
         else:
             # Loop through the verse list and expand the song accordingly.
@@ -596,7 +603,7 @@
                         else:
                             verse_index = VerseType.from_tag(verse[0]['type'])
                         verse_tag = VerseType.translated_tags[verse_index]
-                        verse_def = '%s%s' % (verse_tag, verse[0]['label'])
+                        verse_def = '{tag}{label}'.format(tzg=verse_tag, text=verse[0]['label'])
                         service_item.add_from_text(verse[1], verse_def)
         service_item.title = song.title
         author_list = self.generate_footer(service_item, song)
@@ -639,23 +646,24 @@
         item.raw_footer = []
         item.raw_footer.append(song.title)
         if authors_none:
-            item.raw_footer.append("%s: %s" % (translate('OpenLP.Ui', 'Written by'),
-                                               create_separated_list(authors_none)))
+            item.raw_footer.append("{text}: {authors}".format(text=translate('OpenLP.Ui', 'Written by'),
+                                                              authors=create_separated_list(authors_none)))
         if authors_words_music:
-            item.raw_footer.append("%s: %s" % (AuthorType.Types[AuthorType.WordsAndMusic],
-                                               create_separated_list(authors_words_music)))
+            item.raw_footer.append("{text}: {authors}".format(text=AuthorType.Types[AuthorType.WordsAndMusic],
+                                                              authors=create_separated_list(authors_words_music)))
         if authors_words:
-            item.raw_footer.append("%s: %s" % (AuthorType.Types[AuthorType.Words],
-                                               create_separated_list(authors_words)))
+            item.raw_footer.append("{text}: {authors}".format(text=AuthorType.Types[AuthorType.Words],
+                                                              authors=create_separated_list(authors_words)))
         if authors_music:
-            item.raw_footer.append("%s: %s" % (AuthorType.Types[AuthorType.Music],
-                                               create_separated_list(authors_music)))
+            item.raw_footer.append("{text}: {authors}".format(text=AuthorType.Types[AuthorType.Music],
+                                                              authors=create_separated_list(authors_music)))
         if authors_translation:
-            item.raw_footer.append("%s: %s" % (AuthorType.Types[AuthorType.Translation],
-                                               create_separated_list(authors_translation)))
+            item.raw_footer.append("{text}: {authors}".format(text=AuthorType.Types[AuthorType.Translation],
+                                                              authors=create_separated_list(authors_translation)))
         if song.copyright:
             if self.display_copyright_symbol:
-                item.raw_footer.append("%s %s" % (SongStrings.CopyrightSymbol, song.copyright))
+                item.raw_footer.append("{symbol} {song}".format(symbol=SongStrings.CopyrightSymbol,
+                                                                song=song.copyright))
             else:
                 item.raw_footer.append(song.copyright)
         if self.display_songbook and song.songbook_entries:

=== modified file 'openlp/plugins/songs/lib/openlyricsexport.py'
--- openlp/plugins/songs/lib/openlyricsexport.py	2016-04-05 17:30:20 +0000
+++ openlp/plugins/songs/lib/openlyricsexport.py	2016-05-27 08:20:16 +0000
@@ -61,18 +61,20 @@
             if self.parent.stop_export_flag:
                 return False
             self.parent.increment_progress_bar(
-                translate('SongsPlugin.OpenLyricsExport', 'Exporting "%s"...') % song.title)
+                translate('SongsPlugin.OpenLyricsExport', 'Exporting "{title}"...').format(title=song.title))
             xml = open_lyrics.song_to_xml(song)
             tree = etree.ElementTree(etree.fromstring(xml.encode()))
-            filename = '%s (%s)' % (song.title, ', '.join([author.display_name for author in song.authors]))
+            filename = '{title} ({author})'.format(title=song.title,
+                                                   author=', '.join([author.display_name for author in song.authors]))
             filename = clean_filename(filename)
             # Ensure the filename isn't too long for some filesystems
-            filename_with_ext = '%s.xml' % filename[0:250 - len(self.save_path)]
+            filename_with_ext = '{name}.xml'.format(name=filename[0:250 - len(self.save_path)])
             # Make sure we're not overwriting an existing file
             conflicts = 0
             while os.path.exists(os.path.join(self.save_path, filename_with_ext)):
                 conflicts += 1
-                filename_with_ext = '%s-%d.xml' % (filename[0:247 - len(self.save_path)], conflicts)
+                filename_with_ext = '{name}-{extra}.xml'.format(name=filename[0:247 - len(self.save_path)],
+                                                                extra=conflicts)
             # Pass a file object, because lxml does not cope with some special
             # characters in the path (see lp:757673 and lp:744337).
             tree.write(open(os.path.join(self.save_path, filename_with_ext), 'wb'), encoding='utf-8',

=== modified file 'openlp/plugins/songs/lib/openlyricsxml.py'
--- openlp/plugins/songs/lib/openlyricsxml.py	2016-04-04 19:53:54 +0000
+++ openlp/plugins/songs/lib/openlyricsxml.py	2016-05-27 08:20:16 +0000
@@ -70,6 +70,7 @@
 log = logging.getLogger(__name__)
 
 NAMESPACE = 'http://openlyrics.info/namespace/2009/song'
+# TODO: Verify format() with template variable
 NSMAP = '{' + NAMESPACE + '}' + '%s'
 
 
@@ -126,7 +127,7 @@
         try:
             self.song_xml = objectify.fromstring(xml)
         except etree.XMLSyntaxError:
-            log.exception('Invalid xml %s', xml)
+            log.exception('Invalid xml {text}'.format(text=xml))
         xml_iter = self.song_xml.getiterator()
         for element in xml_iter:
             if element.tag == 'verse':
@@ -422,7 +423,7 @@
         :param tags_element: Some tag elements
         """
         available_tags = FormattingTags.get_html_tags()
-        start_tag = '{%s}' % tag_name
+        start_tag = '{{{name}}}'.format(name=tag_name)
         for tag in available_tags:
             if tag['start tag'] == start_tag:
                 # Create new formatting tag in openlyrics xml.
@@ -449,18 +450,18 @@
             xml_tags = tags_element.xpath('tag/attribute::name')
             # Some formatting tag has only starting part e.g. <br>. Handle this case.
             if tag in end_tags:
-                text = text.replace('{%s}' % tag, '<tag name="%s">' % tag)
+                text = text.replace('{{{tag}}}'.format(tag=tag), '<tag name="{tag}">'.format(tag=tag))
             else:
-                text = text.replace('{%s}' % tag, '<tag name="%s"/>' % tag)
+                text = text.replace('{{{tag}}}'.format(tag=tag), '<tag name="{tag}"/>'.format(tag=tag))
             # Add tag to <format> element if tag not present.
             if tag not in xml_tags:
                 self._add_tag_to_formatting(tag, tags_element)
         # Replace end tags.
         for tag in end_tags:
-            text = text.replace('{/%s}' % tag, '</tag>')
+            text = text.replace('{/{tag}}'.format(tag=tag), '</tag>')
         # Replace \n with <br/>.
         text = text.replace('\n', '<br/>')
-        element = etree.XML('<lines>%s</lines>' % text)
+        element = etree.XML('<lines>{text}</lines>'.format(text=text))
         verse_element.append(element)
         return element
 
@@ -566,9 +567,9 @@
             name = tag.get('name')
             if name is None:
                 continue
-            start_tag = '{%s}' % name[:5]
+            start_tag = '{{{name}}}'.format(name=name[:5])
             # Some tags have only start tag e.g. {br}
-            end_tag = '{/' + name[:5] + '}' if hasattr(tag, 'close') else ''
+            end_tag = '{{/{name}}}'.format(name=name[:5]) if hasattr(tag, 'close') else ''
             openlp_tag = {
                 'desc': name,
                 'start tag': start_tag,
@@ -604,26 +605,30 @@
         text = ''
         use_endtag = True
         # Skip <comment> elements - not yet supported.
+        # TODO: Verify format() with template variables
         if element.tag == NSMAP % 'comment':
             if element.tail:
                 # Append tail text at chord element.
                 text += element.tail
             return text
         # Skip <chord> element - not yet supported.
+        # TODO: Verify format() with template variables
         elif element.tag == NSMAP % 'chord':
             if element.tail:
                 # Append tail text at chord element.
                 text += element.tail
             return text
         # Convert line breaks <br/> to \n.
+        # TODO: Verify format() with template variables
         elif newlines and element.tag == NSMAP % 'br':
             text += '\n'
             if element.tail:
                 text += element.tail
             return text
         # Start formatting tag.
+        # TODO: Verify format() with template variables
         if element.tag == NSMAP % 'tag':
-            text += '{%s}' % element.get('name')
+            text += '{{{name}}}'.format(name=element.get('name'))
             # Some formattings may have only start tag.
             # Handle this case if element has no children and contains no text.
             if not element and not element.text:
@@ -636,8 +641,9 @@
             # Use recursion since nested formatting tags are allowed.
             text += self._process_lines_mixed_content(child, newlines)
         # Append text from tail and add formatting end tag.
+        # TODO: Verify format() with template variables
         if element.tag == NSMAP % 'tag' and use_endtag:
-            text += '{/%s}' % element.get('name')
+            text += '{/{{name}}}'.format(name=element.get('name'))
         # Append text from tail.
         if element.tail:
             text += element.tail
@@ -663,6 +669,7 @@
             # Loop over the "line" elements removing comments and chords.
             for line in element:
                 # Skip comment lines.
+                # TODO: Verify format() with template variables
                 if line.tag == NSMAP % 'comment':
                     continue
                 if text:

=== modified file 'openlp/plugins/songs/lib/songselect.py'
--- openlp/plugins/songs/lib/songselect.py	2016-01-09 18:01:49 +0000
+++ openlp/plugins/songs/lib/songselect.py	2016-05-27 08:20:16 +0000
@@ -78,7 +78,7 @@
         try:
             login_page = BeautifulSoup(self.opener.open(LOGIN_URL).read(), 'lxml')
         except (TypeError, URLError) as e:
-            log.exception('Could not login to SongSelect, %s', e)
+            log.exception('Could not login to SongSelect, {error}'.format(error=e))
             return False
         if callback:
             callback()
@@ -92,7 +92,7 @@
         try:
             posted_page = BeautifulSoup(self.opener.open(LOGIN_URL, data.encode('utf-8')).read(), 'lxml')
         except (TypeError, URLError) as e:
-            log.exception('Could not login to SongSelect, %s', e)
+            log.exception('Could not login to SongSelect, {error}'.format(error=e))
             return False
         if callback:
             callback()
@@ -105,7 +105,7 @@
         try:
             self.opener.open(LOGOUT_URL)
         except (TypeError, URLError) as e:
-            log.exception('Could not log of SongSelect, %s', e)
+            log.exception('Could not log of SongSelect, {error}'.format(error=e))
 
     def search(self, search_text, max_results, callback=None):
         """
@@ -127,7 +127,7 @@
                 results_page = BeautifulSoup(self.opener.open(SEARCH_URL + '?' + urlencode(params)).read(), 'lxml')
                 search_results = results_page.find_all('li', 'result pane')
             except (TypeError, URLError) as e:
-                log.exception('Could not search SongSelect, %s', e)
+                log.exception('Could not search SongSelect, {error}'.format(error=e))
                 search_results = None
             if not search_results:
                 break
@@ -158,7 +158,7 @@
         try:
             song_page = BeautifulSoup(self.opener.open(song['link']).read(), 'lxml')
         except (TypeError, URLError) as e:
-            log.exception('Could not get song from SongSelect, %s', e)
+            log.exception('Could not get song from SongSelect, {error}'.format(error=e))
             return None
         if callback:
             callback()
@@ -203,7 +203,7 @@
             verse_type = VerseType.from_loose_input(verse_type)
             verse_number = int(verse_number)
             song_xml.add_verse_to_lyrics(VerseType.tags[verse_type], verse_number, verse['lyrics'])
-            verse_order.append('%s%s' % (VerseType.tags[verse_type], verse_number))
+            verse_order.append('{tag}{number}'.format(tag=VerseType.tags[verse_type], number=verse_number))
         db_song.verse_order = ' '.join(verse_order)
         db_song.lyrics = song_xml.extract_xml()
         clean_song(self.db_manager, db_song)

=== modified file 'openlp/plugins/songs/lib/songstab.py'
--- openlp/plugins/songs/lib/songstab.py	2015-12-31 22:46:06 +0000
+++ openlp/plugins/songs/lib/songstab.py	2016-05-27 08:20:16 +0000
@@ -74,8 +74,8 @@
                                                           'Import missing songs from service files'))
         self.display_songbook_check_box.setText(translate('SongsPlugin.SongsTab', 'Display songbook in footer'))
         self.display_copyright_check_box.setText(translate('SongsPlugin.SongsTab',
-                                                           'Display "%s" symbol before copyright info') %
-                                                 SongStrings.CopyrightSymbol)
+                                                           'Display "{symbol}" symbol before copyright '
+                                                           'info').format(symbol=SongStrings.CopyrightSymbol))
 
     def on_search_as_type_check_box_changed(self, check_state):
         self.song_search = (check_state == QtCore.Qt.Checked)

=== modified file 'tests/functional/openlp_core_lib/test_projectordb.py'
--- tests/functional/openlp_core_lib/test_projectordb.py	2016-05-21 08:31:24 +0000
+++ tests/functional/openlp_core_lib/test_projectordb.py	2016-05-27 08:20:16 +0000
@@ -265,3 +265,22 @@
                          'manufacturer="IN YOUR DREAMS", model="OpenLP", other="None", sources="None", '
                          'source_list="[]") >',
                          'Projector.__repr__() should have returned a proper representation string')
+
+    def projectorsource_repr_test(self):
+        """
+        Test ProjectorSource.__repr__() text
+        """
+        # GIVEN: test setup
+        projector1 = Projector(**TEST1_DATA)
+        self.projector.add_projector(projector1)
+        item = self.projector.get_projector_by_id(projector1.id)
+        item_id = item.id
+
+        # WHEN: A source entry is saved for item
+        source = ProjectorSource(projector_id=item_id, code='11', text='First RGB source')
+        self.projector.add_source(source)
+
+        # THEN: __repr__ should return a proper string
+        self.assertEqual(str(source),
+                         '<ProjectorSource(id="1", code="11", text="First RGB source", projector_id="1")>',
+                         'ProjectorSource.__repr__)_ should have returned a proper representation string')


Follow ups