From e5a65f69ecb0e939a55dfddc727571df899f8d3c Mon Sep 17 00:00:00 2001 From: un-pogaz <46523284+un-pogaz@users.noreply.github.com> Date: Tue, 28 Jan 2025 09:37:08 +0200 Subject: [PATCH] Automated conversion of % format specifiers Using flint. Change has been filtred because even on safe mode, flint can be too aggressive. --- manual/plugin_examples/editor_demo/main.py | 2 +- pyproject.toml | 15 ++++++++++++ src/calibre/db/tests/writing.py | 2 +- src/calibre/devices/kobo/driver.py | 10 ++++---- src/calibre/devices/mtp/driver.py | 3 +-- src/calibre/devices/mtp/filesystem_cache.py | 2 +- src/calibre/devices/mtp/unix/driver.py | 3 +-- src/calibre/devices/mtp/windows/driver.py | 3 +-- .../devices/smart_device_app/driver.py | 6 ++--- src/calibre/devices/usbms/driver.py | 17 +++++++------- src/calibre/ebooks/conversion/cli.py | 2 +- .../ebooks/conversion/plugins/snb_output.py | 2 +- src/calibre/ebooks/docx/tables.py | 10 ++++---- src/calibre/ebooks/docx/to_html.py | 2 +- src/calibre/ebooks/docx/writer/tables.py | 2 +- src/calibre/ebooks/lit/reader.py | 2 +- src/calibre/ebooks/lrf/objects.py | 10 ++++---- src/calibre/ebooks/lrf/pylrs/pylrf.py | 4 ++-- src/calibre/ebooks/metadata/book/base.py | 6 ++--- src/calibre/ebooks/mobi/debug/headers.py | 6 ++--- src/calibre/ebooks/mobi/debug/index.py | 10 ++++---- src/calibre/ebooks/mobi/debug/mobi6.py | 11 ++++----- src/calibre/ebooks/mobi/debug/mobi8.py | 2 +- src/calibre/ebooks/mobi/reader/markup.py | 12 +++++----- src/calibre/ebooks/mobi/writer2/indexer.py | 5 ++-- src/calibre/ebooks/oeb/polish/subset.py | 2 +- src/calibre/ebooks/oeb/stylizer.py | 4 ++-- src/calibre/ebooks/oeb/transforms/flatcss.py | 8 +++---- src/calibre/ebooks/oeb/transforms/split.py | 6 ++--- src/calibre/ebooks/oeb/transforms/subset.py | 3 +-- src/calibre/ebooks/pdb/ereader/inspector.py | 2 +- src/calibre/ebooks/pdf/reflow.py | 2 +- src/calibre/ebooks/pdf/render/fonts.py | 6 ++--- src/calibre/ebooks/pdf/render/serialize.py | 8 +++---- src/calibre/ebooks/unihandecode/unidecoder.py | 2 +- src/calibre/gui2/actions/store.py | 6 ++--- src/calibre/gui2/add.py | 2 +- src/calibre/gui2/dialogs/duplicates.py | 2 +- src/calibre/gui2/library/models.py | 2 +- src/calibre/gui2/main.py | 2 +- src/calibre/gui2/metadata/bulk_download.py | 2 +- src/calibre/gui2/save.py | 2 +- src/calibre/gui2/tag_browser/model.py | 2 +- src/calibre/gui2/tweak_book/diff/view.py | 2 +- src/calibre/gui2/viewer/printing.py | 2 +- src/calibre/gui2/win_file_dialogs.py | 6 ++--- src/calibre/library/catalogs/bibtex.py | 2 +- src/calibre/library/catalogs/csv_xml.py | 6 ++--- src/calibre/library/catalogs/epub_mobi.py | 2 +- .../library/catalogs/epub_mobi_builder.py | 23 +++++++++---------- src/calibre/linux.py | 2 +- src/calibre/prints.py | 2 +- src/calibre/srv/auth.py | 2 +- src/calibre/srv/auto_reload.py | 2 +- src/calibre/srv/loop.py | 2 +- src/calibre/srv/manage_users_cli.py | 2 +- src/calibre/srv/metadata.py | 2 +- src/calibre/srv/routes.py | 2 +- src/calibre/srv/tests/http.py | 4 ++-- src/calibre/utils/fonts/sfnt/cff/writer.py | 4 ++-- src/calibre/utils/fonts/sfnt/container.py | 3 +-- src/calibre/utils/fonts/sfnt/subset.py | 6 ++--- src/calibre/utils/formatter_functions.py | 2 +- src/calibre/utils/smtplib.py | 4 ++-- src/calibre/utils/winreg/default_programs.py | 2 +- src/calibre/web/feeds/news.py | 4 ++-- src/calibre/web/feeds/recipes/model.py | 2 +- 67 files changed, 151 insertions(+), 151 deletions(-) diff --git a/manual/plugin_examples/editor_demo/main.py b/manual/plugin_examples/editor_demo/main.py index 657aad7727..0815ed478e 100644 --- a/manual/plugin_examples/editor_demo/main.py +++ b/manual/plugin_examples/editor_demo/main.py @@ -116,7 +116,7 @@ class DemoTool(Tool): num = re.search(r'[0-9.]+', val) if num is not None: num = num.group() - val = val.replace(num, '%f' % (float(num) * factor)) + val = val.replace(num, f'{float(num)*factor:f}') style.setProperty('font-size', val) # We should also be dealing with the font shorthand property and # font sizes specified as non numbers, but those are left as exercises diff --git a/pyproject.toml b/pyproject.toml index ed989d9191..93104cffa0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,5 +76,20 @@ docstring-quotes = 'single' inline-quotes = 'single' multiline-quotes = 'single' +[tool.flynt] +line-length = 400 # over value to catch every case +transform-format = false # don't transform already existing format call +exclude = [ + "bypy/", + "setup/polib.py", + "setup/linux-installer.py", + "src/calibre/ebooks/metadata/sources/", + "src/calibre/gui2/store/stores/", + "src/css_selectors/", + "src/polyglot/", + "src/templite/", + "src/tinycss/", +] + [tool.pylsp-mypy] enabled = false diff --git a/src/calibre/db/tests/writing.py b/src/calibre/db/tests/writing.py index b66339558f..1917d25a8a 100644 --- a/src/calibre/db/tests/writing.py +++ b/src/calibre/db/tests/writing.py @@ -756,7 +756,7 @@ class WritingTest(BaseTest): self.assertEqual({1,2,3}, cache.set_sort_for_authors(sdata)) for bid in (1, 2, 3): self.assertIn(', changed', cache.field_for('author_sort', bid)) - sdata = {aid:'%s, changed' % (aid*2 if aid == max(adata) else aid) for aid in adata} + sdata = {aid:f'{aid*2 if aid == max(adata) else aid}, changed' for aid in adata} self.assertEqual({3}, cache.set_sort_for_authors(sdata), 'Setting the author sort to the same value as before, incorrectly marked some books as dirty') # }}} diff --git a/src/calibre/devices/kobo/driver.py b/src/calibre/devices/kobo/driver.py index 3355a1bf5b..206e8f37ac 100644 --- a/src/calibre/devices/kobo/driver.py +++ b/src/calibre/devices/kobo/driver.py @@ -1721,9 +1721,9 @@ class KOBOTOUCH(KOBO): debugging_title = self.debugging_title debug_print(f"KoboTouch:books - set_debugging_title to '{debugging_title}'") bl.set_debugging_title(debugging_title) - debug_print('KoboTouch:books - length bl=%d'%len(bl)) + debug_print(f'KoboTouch:books - length bl={len(bl)}') need_sync = self.parse_metadata_cache(bl, prefix, self.METADATA_CACHE) - debug_print('KoboTouch:books - length bl after sync=%d'%len(bl)) + debug_print(f'KoboTouch:books - length bl after sync={len(bl)}') # make a dict cache of paths so the lookup in the loop below is faster. bl_cache = {} @@ -2266,7 +2266,7 @@ class KOBOTOUCH(KOBO): def upload_books(self, files, names, on_card=None, end_session=True, metadata=None): - debug_print('KoboTouch:upload_books - %d books'%(len(files))) + debug_print(f'KoboTouch:upload_books - {len(files)} books') debug_print('KoboTouch:upload_books - files=', files) if self.modifying_epub(): @@ -2611,7 +2611,7 @@ class KOBOTOUCH(KOBO): # debug_print("KoboTouch:update_device_database_collections - self.bookshelvelist=", self.bookshelvelist) # Process any collections that exist for category, books in collections.items(): - debug_print("KoboTouch:update_device_database_collections - category='%s' books=%d"%(category, len(books))) + debug_print(f"KoboTouch:update_device_database_collections - category='{category}' books={len(books)}") if create_collections and not (category in supportedcategories or category in readstatuslist or category in accessibilitylist): self.check_for_bookshelf(connection, category) # if category in self.bookshelvelist: @@ -2642,7 +2642,7 @@ class KOBOTOUCH(KOBO): category_added = True elif category in self.bookshelvelist and self.supports_bookshelves: if show_debug: - debug_print(' length book.device_collections=%d'%len(book.device_collections)) + debug_print(f' length book.device_collections={len(book.device_collections)}') if category not in book.device_collections: if show_debug: debug_print(' Setting bookshelf on device') diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py index b7a3d0189f..98ef067366 100644 --- a/src/calibre/devices/mtp/driver.py +++ b/src/calibre/devices/mtp/driver.py @@ -27,8 +27,7 @@ from calibre.utils.filenames import shorten_components_to from calibre.utils.icu import lower as icu_lower from polyglot.builtins import as_bytes, iteritems, itervalues -BASE = importlib.import_module('calibre.devices.mtp.%s.driver'%( - 'windows' if iswindows else 'unix')).MTP_DEVICE +BASE = importlib.import_module('calibre.devices.mtp.{}.driver'.format('windows' if iswindows else 'unix')).MTP_DEVICE DEFAULT_THUMBNAIL_HEIGHT = 320 diff --git a/src/calibre/devices/mtp/filesystem_cache.py b/src/calibre/devices/mtp/filesystem_cache.py index 049712f8a3..59d21f678d 100644 --- a/src/calibre/devices/mtp/filesystem_cache.py +++ b/src/calibre/devices/mtp/filesystem_cache.py @@ -108,7 +108,7 @@ class FileOrFolder: path = '' datum = f'size={self.size}' if self.is_folder or self.is_storage: - datum = 'children=%s'%(len(self.files) + len(self.folders)) + datum = f'children={len(self.files)+len(self.folders)}' return f'{name}(id={self.object_id}, storage_id={self.storage_id}, {datum}, path={path}, modified={self.last_mod_string})' __str__ = __repr__ diff --git a/src/calibre/devices/mtp/unix/driver.py b/src/calibre/devices/mtp/unix/driver.py index 5bfc3629ca..115341cb2d 100644 --- a/src/calibre/devices/mtp/unix/driver.py +++ b/src/calibre/devices/mtp/unix/driver.py @@ -339,8 +339,7 @@ class MTP_DEVICE(MTPDeviceBase): prints('There were some errors while getting the ' f' filesystem from {self.current_friendly_name}: {self.format_errorstack(all_errs)}') self._filesystem_cache = FilesystemCache(storage, all_items) - debug('Filesystem metadata loaded in %g seconds (%d objects)'%( - time.time()-st, len(self._filesystem_cache))) + debug(f'Filesystem metadata loaded in {time.time()-st:g} seconds ({len(self._filesystem_cache)} objects)') return self._filesystem_cache @synchronous diff --git a/src/calibre/devices/mtp/windows/driver.py b/src/calibre/devices/mtp/windows/driver.py index 5b9eae38e5..e3d46eab16 100644 --- a/src/calibre/devices/mtp/windows/driver.py +++ b/src/calibre/devices/mtp/windows/driver.py @@ -289,8 +289,7 @@ class MTP_DEVICE(MTPDeviceBase): all_storage.append(storage) items.append(itervalues(id_map)) self._filesystem_cache = FilesystemCache(all_storage, chain(*items)) - debug('Filesystem metadata loaded in %g seconds (%d objects)'%( - time.time()-st, len(self._filesystem_cache))) + debug(f'Filesystem metadata loaded in {time.time()-st:g} seconds ({len(self._filesystem_cache)} objects)') return self._filesystem_cache @same_thread diff --git a/src/calibre/devices/smart_device_app/driver.py b/src/calibre/devices/smart_device_app/driver.py index 315fab2ad5..1ff1a6285a 100644 --- a/src/calibre/devices/smart_device_app/driver.py +++ b/src/calibre/devices/smart_device_app/driver.py @@ -1499,12 +1499,12 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): self.report_progress((i + 1) / float(len(files)), _('Transferring books to device...')) self.report_progress(1.0, _('Transferring books to device...')) - self._debug('finished uploading %d books' % (len(files))) + self._debug(f'finished uploading {len(files)} books') return paths @synchronous('sync_lock') def add_books_to_metadata(self, locations, metadata, booklists): - self._debug('adding metadata for %d books' % (len(metadata))) + self._debug(f'adding metadata for {len(metadata)} books') metadata = iter(metadata) for i, location in enumerate(locations): @@ -1558,7 +1558,7 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): bl.remove_book(book) self._set_known_metadata(book, remove=True) self.report_progress(1.0, _('Removing books from device metadata listing...')) - self._debug('finished removing metadata for %d books' % (len(paths))) + self._debug(f'finished removing metadata for {len(paths)} books') @synchronous('sync_lock') def get_file(self, path, outfile, end_session=True, this_book=None, total_books=None): diff --git a/src/calibre/devices/usbms/driver.py b/src/calibre/devices/usbms/driver.py index e138c81c61..f2598e047d 100644 --- a/src/calibre/devices/usbms/driver.py +++ b/src/calibre/devices/usbms/driver.py @@ -299,8 +299,7 @@ class USBMS(CLI, Device): need_sync = True del bl[idx] - debug_print('USBMS: count found in cache: %d, count of files in metadata: %d, need_sync: %s' % - (len(bl_cache), len(bl), need_sync)) + debug_print(f'USBMS: count found in cache: {len(bl_cache)}, count of files in metadata: {len(bl)}, need_sync: {need_sync}') if need_sync: # self.count_found_in_bl != len(bl) or need_sync: if oncard == 'cardb': self.sync_booklists((None, None, bl)) @@ -315,7 +314,7 @@ class USBMS(CLI, Device): def upload_books(self, files, names, on_card=None, end_session=True, metadata=None): - debug_print('USBMS: uploading %d books'%(len(files))) + debug_print(f'USBMS: uploading {len(files)} books') path = self._sanity_check(on_card, files) @@ -341,7 +340,7 @@ class USBMS(CLI, Device): self.report_progress((i+1) / float(len(files)), _('Transferring books to device...')) self.report_progress(1.0, _('Transferring books to device...')) - debug_print('USBMS: finished uploading %d books'%(len(files))) + debug_print(f'USBMS: finished uploading {len(files)} books') return list(zip(paths, cycle([on_card]))) def upload_cover(self, path, filename, metadata, filepath): @@ -358,7 +357,7 @@ class USBMS(CLI, Device): pass def add_books_to_metadata(self, locations, metadata, booklists): - debug_print('USBMS: adding metadata for %d books'%(len(metadata))) + debug_print(f'USBMS: adding metadata for {len(metadata)} books') metadata = iter(metadata) locations = tuple(locations) @@ -418,7 +417,7 @@ class USBMS(CLI, Device): pass def delete_books(self, paths, end_session=True): - debug_print('USBMS: deleting %d books'%(len(paths))) + debug_print(f'USBMS: deleting {len(paths)} books') for i, path in enumerate(paths): self.report_progress((i+1) / float(len(paths)), _('Removing books from device...')) path = self.normalize_path(path) @@ -428,10 +427,10 @@ class USBMS(CLI, Device): self.delete_extra_book_files(path) self.report_progress(1.0, _('Removing books from device...')) - debug_print('USBMS: finished deleting %d books'%(len(paths))) + debug_print(f'USBMS: finished deleting {len(paths)} books') def remove_books_from_metadata(self, paths, booklists): - debug_print('USBMS: removing metadata for %d books'%(len(paths))) + debug_print(f'USBMS: removing metadata for {len(paths)} books') for i, path in enumerate(paths): self.report_progress((i+1) / float(len(paths)), _('Removing books from device metadata listing...')) @@ -440,7 +439,7 @@ class USBMS(CLI, Device): if path.endswith(book.path): bl.remove_book(book) self.report_progress(1.0, _('Removing books from device metadata listing...')) - debug_print('USBMS: finished removing metadata for %d books'%(len(paths))) + debug_print(f'USBMS: finished removing metadata for {len(paths)} books') # If you override this method and you use book._new_book, then you must # complete the processing before you call this method. The flag is cleared diff --git a/src/calibre/ebooks/conversion/cli.py b/src/calibre/ebooks/conversion/cli.py index 8e46584764..4f8a8cf202 100644 --- a/src/calibre/ebooks/conversion/cli.py +++ b/src/calibre/ebooks/conversion/cli.py @@ -320,7 +320,7 @@ def create_option_parser(args, log): log('\t'+title) except: log('\t'+repr(title)) - log('%d recipes available'%len(titles)) + log(f'{len(titles)} recipes available') raise SystemExit(0) parser = option_parser() diff --git a/src/calibre/ebooks/conversion/plugins/snb_output.py b/src/calibre/ebooks/conversion/plugins/snb_output.py index 3c1f26d5b9..d9fb1a9e7a 100644 --- a/src/calibre/ebooks/conversion/plugins/snb_output.py +++ b/src/calibre/ebooks/conversion/plugins/snb_output.py @@ -163,7 +163,7 @@ class SNBOutput(OutputFormatPlugin): ch.set('src', ProcessFileName(tocitem.href) + '.snbc') ch.text = tocitem.title - etree.SubElement(tocHead, 'chapters').text = '%d' % len(tocBody) + etree.SubElement(tocHead, 'chapters').text = str(len(tocBody)) with open(os.path.join(snbfDir, 'toc.snbf'), 'wb') as f: f.write(etree.tostring(tocInfoTree, pretty_print=True, encoding='utf-8')) diff --git a/src/calibre/ebooks/docx/tables.py b/src/calibre/ebooks/docx/tables.py index c819ee6f3d..61a8ad31c3 100644 --- a/src/calibre/ebooks/docx/tables.py +++ b/src/calibre/ebooks/docx/tables.py @@ -28,9 +28,9 @@ def _read_width(elem, get): elif typ == 'auto': ans = 'auto' elif typ == 'dxa': - ans = '%.3gpt' % (w/20) + ans = f'{w/20:.3g}pt' elif typ == 'pct': - ans = '%.3g%%' % (w/50) + ans = f'{w/50:.3g}%' return ans @@ -243,7 +243,7 @@ class RowStyle(Style): rule, val = self.height if rule != 'auto': try: - c['min-height' if rule == 'atLeast' else 'height'] = '%.3gpt' % (int(val)/20) + c['min-height' if rule == 'atLeast' else 'height'] = f'{int(val)/20:.3g}pt' except (ValueError, TypeError): pass c.update(self.convert_spacing()) @@ -282,7 +282,7 @@ class CellStyle(Style): if val not in (inherit, 'auto'): c[f'padding-{x}'] = val elif val is inherit and x in {'left', 'right'}: - c[f'padding-{x}'] = '%.3gpt' % (115/20) + c[f'padding-{x}'] = f'{115/20:.3g}pt' # In Word, tables are apparently rendered with some default top and # bottom padding irrespective of the cellMargin values. Simulate # that here. @@ -353,7 +353,7 @@ class TableStyle(Style): for x in ('left', 'top', 'right', 'bottom'): val = self.float.get(f'{x}FromText', 0) try: - val = '%.3gpt' % (int(val) / 20) + val = f'{int(val)/20:.3g}pt' except (ValueError, TypeError): val = '0' c[f'margin-{x}'] = val diff --git a/src/calibre/ebooks/docx/to_html.py b/src/calibre/ebooks/docx/to_html.py index eb10488529..fc4fd89e9f 100644 --- a/src/calibre/ebooks/docx/to_html.py +++ b/src/calibre/ebooks/docx/to_html.py @@ -696,7 +696,7 @@ class Convert: else: clear = child.get('clear', None) if clear in {'all', 'left', 'right'}: - br = BR(style='clear:%s'%('both' if clear == 'all' else clear)) + br = BR(style='clear:{}'.format('both' if clear == 'all' else clear)) else: br = BR() text.add_elem(br) diff --git a/src/calibre/ebooks/docx/writer/tables.py b/src/calibre/ebooks/docx/writer/tables.py index 6427af7760..398dbc0b47 100644 --- a/src/calibre/ebooks/docx/writer/tables.py +++ b/src/calibre/ebooks/docx/writer/tables.py @@ -34,7 +34,7 @@ class SpannedCell: def serialize(self, tr, makeelement): tc = makeelement(tr, 'w:tc') tcPr = makeelement(tc, 'w:tcPr') - makeelement(tcPr, 'w:%sMerge' % ('h' if self.horizontal else 'v'), w_val='continue') + makeelement(tcPr, 'w:{}Merge'.format('h' if self.horizontal else 'v'), w_val='continue') makeelement(tc, 'w:p') def applicable_borders(self, edge): diff --git a/src/calibre/ebooks/lit/reader.py b/src/calibre/ebooks/lit/reader.py index d26547471c..15dc7be53e 100644 --- a/src/calibre/ebooks/lit/reader.py +++ b/src/calibre/ebooks/lit/reader.py @@ -320,7 +320,7 @@ class UnBinary: elif state == 'get value': if count == 0xfffe: if not in_censorship: - buf.write(encode('%s"' % (oc - 1))) + buf.write(encode(f'{oc-1}"')) in_censorship = False state = 'get attr' elif count > 0: diff --git a/src/calibre/ebooks/lrf/objects.py b/src/calibre/ebooks/lrf/objects.py index f731c0becc..b527427f1f 100644 --- a/src/calibre/ebooks/lrf/objects.py +++ b/src/calibre/ebooks/lrf/objects.py @@ -178,7 +178,7 @@ class LRFStream(LRFObject): if len(self.stream) != decomp_size: raise LRFParseError('Stream decompressed size is wrong!') if stream.read(2) != b'\x06\xF5': - print('Warning: corrupted end-of-stream tag at %08X; skipping it'%(stream.tell()-2)) + print(f'Warning: corrupted end-of-stream tag at {stream.tell()-2:08X}; skipping it') self.end_stream(None, None) @@ -497,10 +497,10 @@ class TextCSS: fs = getattr(obj, 'fontsize', None) if fs is not None: - ans += item('font-size: %fpt;'%(int(fs)/10)) + ans += item(f'font-size: {int(fs)/10:f}pt;') fw = getattr(obj, 'fontweight', None) if fw is not None: - ans += item('font-weight: %s;'%('bold' if int(fw) >= 700 else 'normal')) + ans += item('font-weight: {};'.format('bold' if int(fw) >= 700 else 'normal')) fn = getattr(obj, 'fontfacename', None) if fn is not None: fn = cls.FONT_MAP[fn] @@ -519,10 +519,10 @@ class TextCSS: ans += item(f'text-align: {al};') lh = getattr(obj, 'linespace', None) if lh is not None: - ans += item('text-align: %fpt;'%(int(lh)/10)) + ans += item(f'text-align: {int(lh)/10:f}pt;') pi = getattr(obj, 'parindent', None) if pi is not None: - ans += item('text-indent: %fpt;'%(int(pi)/10)) + ans += item(f'text-indent: {int(pi)/10:f}pt;') return ans diff --git a/src/calibre/ebooks/lrf/pylrs/pylrf.py b/src/calibre/ebooks/lrf/pylrs/pylrf.py index 8795c97b02..eda9a018cc 100644 --- a/src/calibre/ebooks/lrf/pylrs/pylrf.py +++ b/src/calibre/ebooks/lrf/pylrs/pylrf.py @@ -95,7 +95,7 @@ def writeSignedWord(f, sword): def writeWords(f, *words): - f.write(struct.pack('<%dH' % len(words), *words)) + f.write(struct.pack(f'<{len(words)}H', *words)) def writeDWord(f, dword): @@ -103,7 +103,7 @@ def writeDWord(f, dword): def writeDWords(f, *dwords): - f.write(struct.pack('<%dI' % len(dwords), *dwords)) + f.write(struct.pack(f'<{len(dwords)}I', *dwords)) def writeQWord(f, qword): diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py index 28063336d4..0a69c4110f 100644 --- a/src/calibre/ebooks/metadata/book/base.py +++ b/src/calibre/ebooks/metadata/book/base.py @@ -708,7 +708,7 @@ class Metadata: elif datatype == 'bool': res = _('Yes') if res else _('No') elif datatype == 'rating': - res = '%.2g'%(res/2) + res = f'{res/2:.2g}' elif datatype in ['int', 'float']: try: fmt = cmeta['display'].get('number_format', None) @@ -748,7 +748,7 @@ class Metadata: elif datatype == 'datetime': res = format_date(res, fmeta['display'].get('date_format','dd MMM yyyy')) elif datatype == 'rating': - res = '%.2g'%(res/2) + res = f'{res/2:.2g}' elif key == 'size': res = human_readable(res) return (name, str(res), orig_res, fmeta) @@ -785,7 +785,7 @@ class Metadata: if not self.is_null('languages'): fmt('Languages', ', '.join(self.languages)) if self.rating is not None: - fmt('Rating', ('%.2g'%(float(self.rating)/2)) if self.rating + fmt('Rating', (f'{float(self.rating)/2:.2g}') if self.rating else '') if self.timestamp is not None: fmt('Timestamp', isoformat(self.timestamp)) diff --git a/src/calibre/ebooks/mobi/debug/headers.py b/src/calibre/ebooks/mobi/debug/headers.py index 0adf0c41ac..c97c64264b 100644 --- a/src/calibre/ebooks/mobi/debug/headers.py +++ b/src/calibre/ebooks/mobi/debug/headers.py @@ -500,14 +500,12 @@ class MOBIHeader: # {{{ if self.has_exth: ans += '\n\n' + str(self.exth) - ans += '\n\nBytes after EXTH (%d bytes): %s'%( - len(self.bytes_after_exth), - format_bytes(self.bytes_after_exth)) + ans += f'\n\nBytes after EXTH ({len(self.bytes_after_exth)} bytes): {format_bytes(self.bytes_after_exth)}' ans += '\nNumber of bytes after full name: %d' % (len(self.raw) - (self.fullname_offset + self.fullname_length)) - ans += '\nRecord 0 length: %d'%len(self.raw) + ans += f'\nRecord 0 length: {len(self.raw)}' return ans # }}} diff --git a/src/calibre/ebooks/mobi/debug/index.py b/src/calibre/ebooks/mobi/debug/index.py index 17f7f83617..6b62cb86f5 100644 --- a/src/calibre/ebooks/mobi/debug/index.py +++ b/src/calibre/ebooks/mobi/debug/index.py @@ -98,7 +98,7 @@ class Index: for field in INDEX_HEADER_FIELDS: a('%-12s: %r'%(FIELD_NAMES.get(field, field), self.header[field])) ans.extend(['', '']) - ans += ['*'*10 + ' Index Record Headers (%d records) ' % len(self.index_headers) + '*'*10] + ans += ['*'*10 + f' Index Record Headers ({len(self.index_headers)} records) ' + '*'*10] for i, header in enumerate(self.index_headers): ans += ['*'*10 + ' Index Record %d ' % i + '*'*10] for field in INDEX_HEADER_FIELDS: @@ -111,7 +111,7 @@ class Index: ans.extend(['', '']) if self.table is not None: - a('*'*10 + ' %d Index Entries '%len(self.table) + '*'*10) + a('*'*10 + f' {len(self.table)} Index Entries ' + '*'*10) for k, v in iteritems(self.table): a(f'{k}: {v!r}') @@ -139,8 +139,7 @@ class SKELIndex(Index): for i, text in enumerate(self.table): tag_map = self.table[text] if set(tag_map) != {1, 6}: - raise ValueError('SKEL Index has unknown tags: %s'% - (set(tag_map)-{1,6})) + raise ValueError(f'SKEL Index has unknown tags: {set(tag_map)-{1,6}}') self.records.append(File( i, # file_number text, # name @@ -160,8 +159,7 @@ class SECTIndex(Index): for i, text in enumerate(self.table): tag_map = self.table[text] if set(tag_map) != {2, 3, 4, 6}: - raise ValueError('Chunk Index has unknown tags: %s'% - (set(tag_map)-{2, 3, 4, 6})) + raise ValueError(f'Chunk Index has unknown tags: {set(tag_map)-{2,3,4,6}}') toc_text = self.cncx[tag_map[2][0]] self.records.append(Elem( diff --git a/src/calibre/ebooks/mobi/debug/mobi6.py b/src/calibre/ebooks/mobi/debug/mobi6.py index 6aa93cffcc..29a6a9b014 100644 --- a/src/calibre/ebooks/mobi/debug/mobi6.py +++ b/src/calibre/ebooks/mobi/debug/mobi6.py @@ -197,7 +197,7 @@ class IndexHeader: # {{{ # raise ValueError('Non null trailing bytes after IDXT') def __str__(self): - ans = ['*'*20 + ' Index Header (%d bytes)'%len(self.record.raw)+ '*'*20] + ans = ['*'*20 + f' Index Header ({len(self.record.raw)} bytes)'+ '*'*20] a = ans.append def u(w): @@ -363,8 +363,7 @@ class IndexEntry: # {{{ return [0, 0] def __str__(self): - ans = ['Index Entry(index=%s, length=%d)'%( - self.index, len(self.tags))] + ans = [f'Index Entry(index={self.index}, length={len(self.tags)})'] for tag in self.tags: if tag.value is not None: ans.append('\t'+str(tag)) @@ -412,7 +411,7 @@ class IndexRecord: # {{{ continue def __str__(self): - ans = ['*'*20 + ' Index Entries (%d entries) '%len(self.indices)+ '*'*20] + ans = ['*'*20 + f' Index Entries ({len(self.indices)} entries) '+ '*'*20] a = ans.append def u(w): @@ -470,7 +469,7 @@ class CNCX: # {{{ return self.records.get(offset) def __str__(self): - ans = ['*'*20 + ' cncx (%d strings) '%len(self.records)+ '*'*20] + ans = ['*'*20 + f' cncx ({len(self.records)} strings) '+ '*'*20] for k, v in iteritems(self.records): ans.append('%10d : %s'%(k, v)) return '\n'.join(ans) @@ -568,7 +567,7 @@ class TBSIndexing: # {{{ raise IndexError('Index %d not found'%idx) def __str__(self): - ans = ['*'*20 + ' TBS Indexing (%d records) '%len(self.record_indices)+ '*'*20] + ans = ['*'*20 + f' TBS Indexing ({len(self.record_indices)} records) '+ '*'*20] for r, dat in iteritems(self.record_indices): ans += self.dump_record(r, dat)[-1] return '\n'.join(ans) diff --git a/src/calibre/ebooks/mobi/debug/mobi8.py b/src/calibre/ebooks/mobi/debug/mobi8.py index e9b7c7cea0..0053c1705d 100644 --- a/src/calibre/ebooks/mobi/debug/mobi8.py +++ b/src/calibre/ebooks/mobi/debug/mobi8.py @@ -43,7 +43,7 @@ class FDST: return ans.append(f'{k}: {v}') a('Offset to sections', self.sec_off) a('Number of section records', self.num_sections) - ans.append('**** %d Sections ****'% len(self.sections)) + ans.append(f'**** {len(self.sections)} Sections ****') for sec in self.sections: ans.append('Start: %20d End: %d'%sec) diff --git a/src/calibre/ebooks/mobi/reader/markup.py b/src/calibre/ebooks/mobi/reader/markup.py index 1932237d6c..5152600cc3 100644 --- a/src/calibre/ebooks/mobi/reader/markup.py +++ b/src/calibre/ebooks/mobi/reader/markup.py @@ -80,7 +80,7 @@ def remove_kindlegen_markup(parts, aid_anchor_suffix, linked_aids): aid = None replacement = '' if aid in linked_aids: - replacement = ' id="%s"' % (aid + '-' + aid_anchor_suffix) + replacement = ' id="{}"'.format(aid + '-' + aid_anchor_suffix) tag = within_tag_aid_position_pattern.sub(replacement, tag, 1) srcpieces[j] = tag part = ''.join(srcpieces) @@ -150,7 +150,7 @@ def update_flow_links(mobi8_reader, resource_map, log): num = int(m.group(1), 32) href = resource_map[num-1] if href: - replacement = '"%s"'%('../'+ href) + replacement = '"{}"'.format('../'+ href) tag = img_index_pattern.sub(replacement, tag, 1) else: log.warn(f'Referenced image {num} was not recognized ' @@ -168,7 +168,7 @@ def update_flow_links(mobi8_reader, resource_map, log): num = int(m.group(1), 32) href = resource_map[num-1] if href: - replacement = '"%s"'%('../'+ href) + replacement = '"{}"'.format('../'+ href) tag = url_img_index_pattern.sub(replacement, tag, 1) else: log.warn(f'Referenced image {num} was not recognized as a ' @@ -182,9 +182,9 @@ def update_flow_links(mobi8_reader, resource_map, log): log.warn(f'Referenced font {num} was not recognized as a ' f'valid font in {tag}') else: - replacement = '"%s"'%('../'+ href) + replacement = '"{}"'.format('../'+ href) if href.endswith('.failed'): - replacement = '"%s"'%('failed-'+href) + replacement = '"{}"'.format('failed-'+href) tag = font_index_pattern.sub(replacement, tag, 1) # process links to other css pieces @@ -280,7 +280,7 @@ def insert_images_into_markup(parts, resource_map, log): except IndexError: href = '' if href: - replacement = '"%s"'%('../' + href) + replacement = '"{}"'.format('../' + href) tag = img_index_pattern.sub(replacement, tag, 1) else: log.warn(f'Referenced image {num} was not recognized as ' diff --git a/src/calibre/ebooks/mobi/writer2/indexer.py b/src/calibre/ebooks/mobi/writer2/indexer.py index a6ee0779e2..cf847bdcb8 100644 --- a/src/calibre/ebooks/mobi/writer2/indexer.py +++ b/src/calibre/ebooks/mobi/writer2/indexer.py @@ -447,8 +447,7 @@ class Indexer: # {{{ if self.is_periodical and self.masthead_offset is None: raise ValueError('Periodicals must have a masthead') - self.log('Generating MOBI index for a %s'%('periodical' if - self.is_periodical else 'book')) + self.log('Generating MOBI index for a {}'.format('periodical' if self.is_periodical else 'book')) self.is_flat_periodical = False if self.is_periodical: periodical_node = next(iter(oeb.toc)) @@ -530,7 +529,7 @@ class Indexer: # {{{ ans = header + body if len(ans) > 0x10000: - raise ValueError('Too many entries (%d) in the TOC'%len(offsets)) + raise ValueError(f'Too many entries ({len(offsets)}) in the TOC') return ans # }}} diff --git a/src/calibre/ebooks/oeb/polish/subset.py b/src/calibre/ebooks/oeb/polish/subset.py index a3ea260d42..d7ae221861 100644 --- a/src/calibre/ebooks/oeb/polish/subset.py +++ b/src/calibre/ebooks/oeb/polish/subset.py @@ -82,7 +82,7 @@ def subset_all_fonts(container, font_stats, report): report(_('The font %s was already subset')%font_name) else: report(_('Decreased the font {0} to {1} of its original size').format( - font_name, ('%.1f%%' % (nlen/olen * 100)))) + font_name, (f'{nlen/olen*100:.1f}%'))) changed = True f.seek(0), f.truncate(), f.write(nraw) diff --git a/src/calibre/ebooks/oeb/stylizer.py b/src/calibre/ebooks/oeb/stylizer.py index 66b7a132ae..7e872eeff5 100644 --- a/src/calibre/ebooks/oeb/stylizer.py +++ b/src/calibre/ebooks/oeb/stylizer.py @@ -184,7 +184,7 @@ class StylizerRules: if size == 'smallest': size = 'xx-small' if size in FONT_SIZE_NAMES: - style['font-size'] = '%.1frem' % (self.profile.fnames[size] / float(self.profile.fbase)) + style['font-size'] = f'{self.profile.fnames[size]/float(self.profile.fbase):.1f}rem' if '-epub-writing-mode' in style: for x in ('-webkit-writing-mode', 'writing-mode'): style[x] = style.get(x, style['-epub-writing-mode']) @@ -419,7 +419,7 @@ class Stylizer: style['font-size'].endswith('pt'): style = copy.copy(style) size = float(style['font-size'][:-2]) - style['font-size'] = '%.2fpt' % (size * font_scale) + style['font-size'] = f'{size*font_scale:.2f}pt' style = ';\n '.join(': '.join(item) for item in style.items()) rules.append(f'{selector} {{\n {style};\n}}') return '\n'.join(rules) diff --git a/src/calibre/ebooks/oeb/transforms/flatcss.py b/src/calibre/ebooks/oeb/transforms/flatcss.py index 535b795603..ab3b81d281 100644 --- a/src/calibre/ebooks/oeb/transforms/flatcss.py +++ b/src/calibre/ebooks/oeb/transforms/flatcss.py @@ -350,7 +350,7 @@ class CSSFlattener: if value == 0 or not isinstance(value, numbers.Number): continue if value <= slineh: - cssdict[property] = '%0.5fem' % (dlineh / fsize) + cssdict[property] = f'{dlineh/fsize:0.5f}em' else: try: value = round(value / slineh) * dlineh @@ -358,7 +358,7 @@ class CSSFlattener: self.oeb.logger.warning( 'Invalid length:', value) value = 0.0 - cssdict[property] = '%0.5fem' % (value / fsize) + cssdict[property] = f'{value/fsize:0.5f}em' def flatten_node(self, node, stylizer, names, styles, pseudo_styles, psize, item_id, recurse=True): if not isinstance(node.tag, string_or_bytes) or namespace(node.tag) not in (XHTML_NS, SVG_NS): @@ -473,12 +473,12 @@ class CSSFlattener: dyn_rescale = 1 fsize = self.fmap[_sbase] fsize *= dyn_rescale - cssdict['font-size'] = '%0.5fem'%(fsize/psize) + cssdict['font-size'] = f'{fsize/psize:0.5f}em' psize = fsize elif 'font-size' in cssdict or tag == 'body': fsize = self.fmap[font_size] try: - cssdict['font-size'] = '%0.5fem' % (fsize / psize) + cssdict['font-size'] = f'{fsize/psize:0.5f}em' except ZeroDivisionError: cssdict['font-size'] = f'{fsize:.1f}pt' psize = fsize diff --git a/src/calibre/ebooks/oeb/transforms/split.py b/src/calibre/ebooks/oeb/transforms/split.py index daea59fbcc..933750e318 100644 --- a/src/calibre/ebooks/oeb/transforms/split.py +++ b/src/calibre/ebooks/oeb/transforms/split.py @@ -234,7 +234,7 @@ class FlowSplitter: self.was_split = len(self.trees) > 1 if self.was_split: - self.log('\tSplit into %d parts'%len(self.trees)) + self.log(f'\tSplit into {len(self.trees)} parts') self.commit() def split_on_page_breaks(self, orig_tree): @@ -309,10 +309,10 @@ class FlowSplitter: return True def split_text(self, text, root, size): - self.log.debug('\t\t\tSplitting text of length: %d'%len(text)) + self.log.debug(f'\t\t\tSplitting text of length: {len(text)}') rest = text.replace('\r', '') parts = rest.split('\n\n') - self.log.debug('\t\t\t\tFound %d parts'%len(parts)) + self.log.debug(f'\t\t\t\tFound {len(parts)} parts') if max(map(len, parts)) > size: raise SplitError('Cannot split as file contains a
 tag '
                 'with a very large paragraph', root)
diff --git a/src/calibre/ebooks/oeb/transforms/subset.py b/src/calibre/ebooks/oeb/transforms/subset.py
index cd59471bac..cd9f0ebbe0 100644
--- a/src/calibre/ebooks/oeb/transforms/subset.py
+++ b/src/calibre/ebooks/oeb/transforms/subset.py
@@ -179,8 +179,7 @@ class SubsetFonts:
             font['item'].unload_data_from_memory()
 
         if totals[0]:
-            self.log('Reduced total font size to %.1f%% of original'%
-                    (totals[0]/totals[1] * 100))
+            self.log(f'Reduced total font size to {totals[0]/totals[1]*100:.1f}% of original')
 
     def find_embedded_fonts(self):
         '''
diff --git a/src/calibre/ebooks/pdb/ereader/inspector.py b/src/calibre/ebooks/pdb/ereader/inspector.py
index b9bb19a8d7..938492f77d 100644
--- a/src/calibre/ebooks/pdb/ereader/inspector.py
+++ b/src/calibre/ebooks/pdb/ereader/inspector.py
@@ -28,7 +28,7 @@ def ereader_header_info(header):
         print('')
         ereader_header_info202(h0)
     else:
-        raise EreaderError('Size mismatch. eReader header record size %i KB is not supported.' % len(h0))
+        raise EreaderError(f'Size mismatch. eReader header record size {len(h0)} KB is not supported.')
 
 
 def pdb_header_info(header):
diff --git a/src/calibre/ebooks/pdf/reflow.py b/src/calibre/ebooks/pdf/reflow.py
index aa3d5762a8..ff82cffa22 100644
--- a/src/calibre/ebooks/pdf/reflow.py
+++ b/src/calibre/ebooks/pdf/reflow.py
@@ -696,7 +696,7 @@ class Region:
 
     def dump(self, f):
         f.write('############################################################\n')
-        f.write('########## Region (%d columns) ###############\n'%len(self.columns))
+        f.write(f'########## Region ({len(self.columns)} columns) ###############\n')
         f.write('############################################################\n\n')
         for i, col in enumerate(self.columns):
             col.dump(f, i)
diff --git a/src/calibre/ebooks/pdf/render/fonts.py b/src/calibre/ebooks/pdf/render/fonts.py
index 2118a6720e..dec0355783 100644
--- a/src/calibre/ebooks/pdf/render/fonts.py
+++ b/src/calibre/ebooks/pdf/render/fonts.py
@@ -107,7 +107,7 @@ class CMap(Stream):
         mapping = []
         for m in maps:
             meat = '\n'.join(f'{k} {v}' for k, v in iteritems(m))
-            mapping.append('%d beginbfchar\n%s\nendbfchar'%(len(m), meat))
+            mapping.append(f'{len(m)} beginbfchar\n{meat}\nendbfchar')
         try:
             name = name.encode('ascii').decode('ascii')
         except Exception:
@@ -177,8 +177,8 @@ class Font:
         except NoGlyphs:
             if self.used_glyphs:
                 debug(
-                    'Subsetting of %s failed, font appears to have no glyphs for the %d characters it is used with, some text may not be rendered in the PDF' %
-                    (self.metrics.names.get('full_name', 'Unknown'), len(self.used_glyphs)))
+                    'Subsetting of {} failed, font appears to have no glyphs for the {} characters it is used with, some text may not be rendered in the PDF'
+                    .format(self.metrics.names.get('full_name', 'Unknown'), len(self.used_glyphs)))
         if self.is_otf:
             self.font_stream.write(self.metrics.sfnt['CFF '].raw)
         else:
diff --git a/src/calibre/ebooks/pdf/render/serialize.py b/src/calibre/ebooks/pdf/render/serialize.py
index cdcc9a7da0..a6c1538ca5 100644
--- a/src/calibre/ebooks/pdf/render/serialize.py
+++ b/src/calibre/ebooks/pdf/render/serialize.py
@@ -92,24 +92,24 @@ class Page(Stream):
 
     def set_opacity(self, opref):
         if opref not in self.opacities:
-            self.opacities[opref] = 'Opa%d'%len(self.opacities)
+            self.opacities[opref] = f'Opa{len(self.opacities)}'
         name = self.opacities[opref]
         serialize(Name(name), self)
         self.write(b' gs ')
 
     def add_font(self, fontref):
         if fontref not in self.fonts:
-            self.fonts[fontref] = 'F%d'%len(self.fonts)
+            self.fonts[fontref] = f'F{len(self.fonts)}'
         return self.fonts[fontref]
 
     def add_image(self, imgref):
         if imgref not in self.xobjects:
-            self.xobjects[imgref] = 'Image%d'%len(self.xobjects)
+            self.xobjects[imgref] = f'Image{len(self.xobjects)}'
         return self.xobjects[imgref]
 
     def add_pattern(self, patternref):
         if patternref not in self.patterns:
-            self.patterns[patternref] = 'Pat%d'%len(self.patterns)
+            self.patterns[patternref] = f'Pat{len(self.patterns)}'
         return self.patterns[patternref]
 
     def add_resources(self):
diff --git a/src/calibre/ebooks/unihandecode/unidecoder.py b/src/calibre/ebooks/unihandecode/unidecoder.py
index aba5069030..0bac7ed26e 100644
--- a/src/calibre/ebooks/unihandecode/unidecoder.py
+++ b/src/calibre/ebooks/unihandecode/unidecoder.py
@@ -95,7 +95,7 @@ class Unidecoder:
         # Code groups within CODEPOINTS take the form 'xAB'
         if not isinstance(character, str):
             character = str(character, 'utf-8')
-        return 'x%02x' % (ord(character) >> 8)
+        return f'x{ord(character) >> 8:02x}'
 
     def grouped_point(self, character):
         '''
diff --git a/src/calibre/gui2/actions/store.py b/src/calibre/gui2/actions/store.py
index 69ea2ffc8b..ddb750c3d7 100644
--- a/src/calibre/gui2/actions/store.py
+++ b/src/calibre/gui2/actions/store.py
@@ -23,10 +23,8 @@ class StoreAction(InterfaceAction):
         self.qaction.triggered.connect(self.do_search)
         self.store_menu = self.qaction.menu()
         cm = partial(self.create_menu_action, self.store_menu)
-        for x, t in [('author', _('this author')), ('title', _('this title')),
-                ('book', _('this book'))]:
-            func = getattr(self, 'search_%s'%('author_title' if x == 'book'
-                else x))
+        for x, t in [('author', _('this author')), ('title', _('this title')), ('book', _('this book'))]:
+            func = getattr(self, 'search_{}'.format('author_title' if x == 'book' else x))
             ac = cm(x, _('Search for %s')%t, triggered=func)
             setattr(self, 'action_search_by_'+x, ac)
         self.store_menu.addSeparator()
diff --git a/src/calibre/gui2/add.py b/src/calibre/gui2/add.py
index c086f820fd..0f5bde7f70 100644
--- a/src/calibre/gui2/add.py
+++ b/src/calibre/gui2/add.py
@@ -474,7 +474,7 @@ class Adder(QObject):
             # detection/automerge will fail for this book.
             traceback.print_exc()
         if DEBUG:
-            prints('Added', mi.title, 'to db in: %.1f' % (time.time() - st))
+            prints('Added', mi.title, f'to db in: {time.time()-st:.1f}')
 
     def add_formats(self, book_id, paths, mi, replace=True, is_an_add=False):
         fmap = {p.rpartition(os.path.extsep)[-1].lower():p for p in paths}
diff --git a/src/calibre/gui2/dialogs/duplicates.py b/src/calibre/gui2/dialogs/duplicates.py
index fe5f73ed98..df09200cac 100644
--- a/src/calibre/gui2/dialogs/duplicates.py
+++ b/src/calibre/gui2/dialogs/duplicates.py
@@ -23,7 +23,7 @@ class DuplicatesQuestion(QDialog):
         self.setLayout(l)
         t = ngettext('Duplicate found', 'duplicates found', len(duplicates))
         if len(duplicates) > 1:
-            t = '%d %s' % (len(duplicates), t)
+            t = f'{len(duplicates)} {t}'
         self.setWindowTitle(t)
         self.i = i = QIcon.ic('dialog_question.png')
         self.setWindowIcon(i)
diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py
index aaaa9fb67a..ed80eaf09a 100644
--- a/src/calibre/gui2/library/models.py
+++ b/src/calibre/gui2/library/models.py
@@ -888,7 +888,7 @@ class BooksModel(QAbstractTableModel):  # {{{
                     val = fffunc(field_obj, idfunc(idx), default_value=0) or 0
                     if val == 0:
                         return None
-                    ans = '%.1f' % (val * sz_mult)
+                    ans = f'{val*sz_mult:.1f}'
                     return ('<0.1' if ans == '0.0' else ans)
             elif field == 'languages':
                 def func(idx):
diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py
index 3d1858a791..1b21848c01 100644
--- a/src/calibre/gui2/main.py
+++ b/src/calibre/gui2/main.py
@@ -255,7 +255,7 @@ class GuiRunner(QObject):
                 self.splash_screen.finish(main)
                 timed_print('splash screen hidden')
             self.splash_screen = None
-        timed_print('Started up in %.2f seconds'%(monotonic() - self.startup_time), 'with', len(db.data), 'books')
+        timed_print(f'Started up in {monotonic()-self.startup_time:.2f} seconds', 'with', len(db.data), 'books')
         main.set_exception_handler()
         if len(self.args) > 1:
             main.handle_cli_args(self.args[1:])
diff --git a/src/calibre/gui2/metadata/bulk_download.py b/src/calibre/gui2/metadata/bulk_download.py
index 1b2dae77dc..efabc7f70e 100644
--- a/src/calibre/gui2/metadata/bulk_download.py
+++ b/src/calibre/gui2/metadata/bulk_download.py
@@ -273,7 +273,7 @@ def download(all_ids, tf, db, do_identify, covers, ensure_fields,
 
         if abort.is_set():
             aborted = True
-        log('Download complete, with %d failures'%len(failed_ids))
+        log(f'Download complete, with {len(failed_ids)} failures')
         return (aborted, ans, tdir, tf, failed_ids, failed_covers, title_map,
                 lm_map, all_failed)
     finally:
diff --git a/src/calibre/gui2/save.py b/src/calibre/gui2/save.py
index cb5457c326..043eb60a7e 100644
--- a/src/calibre/gui2/save.py
+++ b/src/calibre/gui2/save.py
@@ -342,7 +342,7 @@ class Saver(QObject):
 
     def updating_metadata_finished(self):
         if DEBUG:
-            prints('Saved %d books in %.1f seconds' % (len(self.all_book_ids), time.time() - self.start_time))
+            prints(f'Saved {len(self.all_book_ids)} books in {time.time()-self.start_time:.1f} seconds')
         self.pd.close()
         self.pd.deleteLater()
         self.report()
diff --git a/src/calibre/gui2/tag_browser/model.py b/src/calibre/gui2/tag_browser/model.py
index 7eb4a401a6..f17af8c0f8 100644
--- a/src/calibre/gui2/tag_browser/model.py
+++ b/src/calibre/gui2/tag_browser/model.py
@@ -1968,7 +1968,7 @@ class TagsModel(QAbstractItemModel):  # {{{
                     if tag.name and tag.name[0] in stars:  # char is a star or a half. Assume rating
                         rnum = len(tag.name)
                         if tag.name.endswith(stars[-1]):
-                            rnum = '%s.5' % (rnum - 1)
+                            rnum = f'{rnum-1}.5'
                         ans.append(f'{prefix}{category}:{rnum}')
                     else:
                         name = tag.original_name
diff --git a/src/calibre/gui2/tweak_book/diff/view.py b/src/calibre/gui2/tweak_book/diff/view.py
index 9a0f849c76..6b0b1a084c 100644
--- a/src/calibre/gui2/tweak_book/diff/view.py
+++ b/src/calibre/gui2/tweak_book/diff/view.py
@@ -904,7 +904,7 @@ class DiffSplit(QSplitter):  # {{{
                     continue
 
                 if tag in {'replace', 'insert', 'delete'}:
-                    fmt = getattr(self.left, '%s_format' % ('replacereplace' if tag == 'replace' else tag))
+                    fmt = getattr(self.left, '{}_format'.format('replacereplace' if tag == 'replace' else tag))
                     f = QTextLayout.FormatRange()
                     f.start, f.length, f.format = pos, len(word), fmt
                     fmts.append(f)
diff --git a/src/calibre/gui2/viewer/printing.py b/src/calibre/gui2/viewer/printing.py
index 3f1d608744..be54559b45 100644
--- a/src/calibre/gui2/viewer/printing.py
+++ b/src/calibre/gui2/viewer/printing.py
@@ -188,7 +188,7 @@ def do_print():
     if data['page_numbers']:
         args.append('--pdf-page-numbers')
     for edge in 'left top right bottom'.split():
-        args.append('--pdf-page-margin-' + edge), args.append('%.1f' % (data['margin_' + edge] * 72))
+        args.append('--pdf-page-margin-' + edge), args.append('{:.1f}'.format(data['margin_' + edge] * 72))
     from calibre.ebooks.conversion.cli import main
     main(args)
 
diff --git a/src/calibre/gui2/win_file_dialogs.py b/src/calibre/gui2/win_file_dialogs.py
index 8c6f1b3f26..785418f64a 100644
--- a/src/calibre/gui2/win_file_dialogs.py
+++ b/src/calibre/gui2/win_file_dialogs.py
@@ -62,16 +62,16 @@ def serialize_string(key, val):
     val = str(val).encode('utf-8')
     if len(val) > 2**16 - 1:
         raise ValueError(f'{key} is too long')
-    return struct.pack('=B%dsH%ds' % (len(key), len(val)), len(key), key, len(val), val)
+    return struct.pack(f'=B{len(key)}sH{len(val)}s', len(key), key, len(val), val)
 
 
 def serialize_file_types(file_types):
     key = b'FILE_TYPES'
-    buf = [struct.pack('=B%dsH' % len(key), len(key), key, len(file_types))]
+    buf = [struct.pack(f'=B{len(key)}sH', len(key), key, len(file_types))]
 
     def add(x):
         x = x.encode('utf-8').replace(b'\0', b'')
-        buf.append(struct.pack('=H%ds' % len(x), len(x), x))
+        buf.append(struct.pack(f'=H{len(x)}s', len(x), x))
     for name, extensions in file_types:
         add(name or _('Files'))
         if isinstance(extensions, string_or_bytes):
diff --git a/src/calibre/library/catalogs/bibtex.py b/src/calibre/library/catalogs/bibtex.py
index bdc0ab1ebb..b58f91d06b 100644
--- a/src/calibre/library/catalogs/bibtex.py
+++ b/src/calibre/library/catalogs/bibtex.py
@@ -315,7 +315,7 @@ class BIBTEX(CatalogPlugin):
                 log(" --search='{}'".format(opts_dict['search_text']))
 
             if opts_dict['ids']:
-                log(' Book count: %d' % len(opts_dict['ids']))
+                log(' Book count: {}'.format(len(opts_dict['ids'])))
                 if opts_dict['search_text']:
                     log(' (--search ignored when a subset of the database is specified)')
 
diff --git a/src/calibre/library/catalogs/csv_xml.py b/src/calibre/library/catalogs/csv_xml.py
index 433955b5e2..510e446283 100644
--- a/src/calibre/library/catalogs/csv_xml.py
+++ b/src/calibre/library/catalogs/csv_xml.py
@@ -75,7 +75,7 @@ class CSV_XML(CatalogPlugin):
                 log(" --search='{}'".format(opts_dict['search_text']))
 
             if opts_dict['ids']:
-                log(' Book count: %d' % len(opts_dict['ids']))
+                log(' Book count: {}'.format(len(opts_dict['ids'])))
                 if opts_dict['search_text']:
                     log(' (--search ignored when a subset of the database is specified)')
 
@@ -153,7 +153,7 @@ class CSV_XML(CatalogPlugin):
                         item = item.replace('\r\n', ' ')
                         item = item.replace('\n', ' ')
                     elif fm.get(field, {}).get('datatype', None) == 'rating' and item:
-                        item = '%.2g' % (item / 2)
+                        item = f'{item/2:.2g}'
 
                     # Convert HTML to markdown text
                     if isinstance(item, str):
@@ -197,7 +197,7 @@ class CSV_XML(CatalogPlugin):
                             if not isinstance(val, (bytes, str)):
                                 if (fm.get(field, {}).get('datatype', None) ==
                                         'rating' and val):
-                                    val = '%.2g' % (val / 2)
+                                    val = f'{val/2:.2g}'
                                 val = str(val)
                             item = getattr(E, field)(val)
                             record.append(item)
diff --git a/src/calibre/library/catalogs/epub_mobi.py b/src/calibre/library/catalogs/epub_mobi.py
index 1b73592bc6..3e56266ac1 100644
--- a/src/calibre/library/catalogs/epub_mobi.py
+++ b/src/calibre/library/catalogs/epub_mobi.py
@@ -300,7 +300,7 @@ class EPUB_MOBI(CatalogPlugin):
 
         opts_dict = vars(opts)
         if opts_dict['ids']:
-            build_log.append(' book count: %d' % len(opts_dict['ids']))
+            build_log.append(' book count: {}'.format(len(opts_dict['ids'])))
 
         sections_list = []
         if opts.generate_authors:
diff --git a/src/calibre/library/catalogs/epub_mobi_builder.py b/src/calibre/library/catalogs/epub_mobi_builder.py
index 6b50adee01..7a00b1b0e5 100644
--- a/src/calibre/library/catalogs/epub_mobi_builder.py
+++ b/src/calibre/library/catalogs/epub_mobi_builder.py
@@ -834,11 +834,11 @@ class CatalogBuilder:
         self.individual_authors = list(individual_authors)
 
         if self.DEBUG and self.opts.verbose:
-            self.opts.log.info('\nfetch_books_by_author(): %d unique authors' % len(unique_authors))
+            self.opts.log.info(f'\nfetch_books_by_author(): {len(unique_authors)} unique authors')
             for author in unique_authors:
                 self.opts.log.info((' %-50s %-25s %2d' % (author[0][0:45], author[1][0:20],
                     author[2])).encode('utf-8'))
-            self.opts.log.info('\nfetch_books_by_author(): %d individual authors' % len(individual_authors))
+            self.opts.log.info(f'\nfetch_books_by_author(): {len(individual_authors)} individual authors')
             for author in sorted(individual_authors):
                 self.opts.log.info(f'{author}')
 
@@ -865,7 +865,7 @@ class CatalogBuilder:
             self.books_by_title = sorted(self.books_to_catalog, key=lambda x: sort_key(x['title_sort'].upper()))
 
             if self.DEBUG and self.opts.verbose:
-                self.opts.log.info('fetch_books_by_title(): %d books' % len(self.books_by_title))
+                self.opts.log.info(f'fetch_books_by_title(): {len(self.books_by_title)} books')
                 self.opts.log.info(' %-40s %-40s' % ('title', 'title_sort'))
                 for title in self.books_by_title:
                     self.opts.log.info((' %-40s %-40s' % (title['title'][0:40],
@@ -1202,7 +1202,7 @@ class CatalogBuilder:
                     else:
                         yield tag
 
-            ans = '%s%d %s:\n' % (' ' * indent, len(tags), header)
+            ans = '{}{} {}:\n'.format(' '*indent, len(tags), header)
             ans += ' ' * (indent + 1)
             out_str = ''
             sorted_tags = sorted(tags, key=sort_key)
@@ -2119,12 +2119,11 @@ class CatalogBuilder:
 
         if self.opts.verbose:
             if len(genre_list):
-                self.opts.log.info('  Genre summary: %d active genre tags used in generating catalog with %d titles' %
-                                (len(genre_list), len(self.books_to_catalog)))
+                self.opts.log.info(f'  Genre summary: {len(genre_list)} active genre tags used in generating catalog with {len(self.books_to_catalog)} titles')
 
                 for genre in genre_list:
                     for key in genre:
-                        self.opts.log.info('   %s: %d %s' % (self.get_friendly_genre_tag(key),
+                        self.opts.log.info('   {}: {} {}'.format(self.get_friendly_genre_tag(key),
                                             len(genre[key]),
                                             'titles' if len(genre[key]) > 1 else 'title'))
 
@@ -3036,7 +3035,7 @@ class CatalogBuilder:
         Outputs:
          ncx_soup (file): updated
         '''
-        section_header = '%s [%d]' % (tocTitle, len(self.books_by_description))
+        section_header = f'{tocTitle} [{len(self.books_by_description)}]'
         if self.generate_for_kindle_mobi:
             section_header = tocTitle
         navPointTag = self.generate_ncx_section_header('bydescription-ID', section_header, 'content/book_%d.html' % int(self.books_by_description[0]['id']))
@@ -3115,7 +3114,7 @@ class CatalogBuilder:
             series_by_letter.append(current_series_list)
 
         # --- Construct the 'Books By Series' section ---
-        section_header = '%s [%d]' % (tocTitle, len(self.all_series))
+        section_header = f'{tocTitle} [{len(self.all_series)}]'
         if self.generate_for_kindle_mobi:
             section_header = tocTitle
         output = 'BySeries'
@@ -3197,7 +3196,7 @@ class CatalogBuilder:
             books_by_letter.append(current_book_list)
 
         # --- Construct the 'Books By Title' section ---
-        section_header = '%s [%d]' % (tocTitle, len(self.books_by_title))
+        section_header = f'{tocTitle} [{len(self.books_by_title)}]'
         if self.generate_for_kindle_mobi:
             section_header = tocTitle
         output = 'ByAlphaTitle'
@@ -3285,7 +3284,7 @@ class CatalogBuilder:
         # --- Construct the 'Books By Author' *section* ---
         file_ID = f'{tocTitle.lower()}'
         file_ID = file_ID.replace(' ', '')
-        section_header = '%s [%d]' % (tocTitle, len(self.individual_authors))
+        section_header = f'{tocTitle} [{len(self.individual_authors)}]'
         if self.generate_for_kindle_mobi:
             section_header = tocTitle
         navPointTag = self.generate_ncx_section_header(f'{file_ID}-ID', section_header, f'{HTML_file}#section_start')
@@ -3563,7 +3562,7 @@ class CatalogBuilder:
         # --- Construct the 'Books By Genre' *section* ---
         file_ID = f'{tocTitle.lower()}'
         file_ID = file_ID.replace(' ', '')
-        section_header = '%s [%d]' % (tocTitle, len(self.genres))
+        section_header = f'{tocTitle} [{len(self.genres)}]'
         if self.generate_for_kindle_mobi:
             section_header = tocTitle
         navPointTag = self.generate_ncx_section_header(f'{file_ID}-ID', section_header, 'content/Genre_{}.html#section_start'.format(self.genres[0]['tag']))
diff --git a/src/calibre/linux.py b/src/calibre/linux.py
index 817def4064..c81188de5f 100644
--- a/src/calibre/linux.py
+++ b/src/calibre/linux.py
@@ -798,7 +798,7 @@ class PostInstall:
                     os.rmdir(config_dir)
 
         if warn is None and self.warnings:
-            self.info('\n\nThere were %d warnings\n'%len(self.warnings))
+            self.info(f'\n\nThere were {len(self.warnings)} warnings\n')
             for args, kwargs in self.warnings:
                 self.info('*', *args, **kwargs)
                 print()
diff --git a/src/calibre/prints.py b/src/calibre/prints.py
index 14120b0e3b..ba39f8a3b5 100644
--- a/src/calibre/prints.py
+++ b/src/calibre/prints.py
@@ -79,4 +79,4 @@ def debug_print(*args, **kw):
     # Check if debugging is enabled
     if is_debugging():
         # Print the elapsed time and the provided arguments if debugging is enabled
-        prints('DEBUG: %6.1f' % (time.monotonic() - base_time), *args, **kw)
+        prints(f'DEBUG: {time.monotonic()-base_time:6.1f}', *args, **kw)
diff --git a/src/calibre/srv/auth.py b/src/calibre/srv/auth.py
index 2057acf024..4bf13a0cf6 100644
--- a/src/calibre/srv/auth.py
+++ b/src/calibre/srv/auth.py
@@ -271,7 +271,7 @@ class AuthController:
     def do_http_auth(self, data, endpoint):
         ban_key = data.remote_addr, data.forwarded_for
         if self.ban_list.is_banned(ban_key):
-            raise HTTPForbidden('Too many login attempts', log='Too many login attempts from: %s' % (ban_key if data.forwarded_for else data.remote_addr))
+            raise HTTPForbidden('Too many login attempts', log=f'Too many login attempts from: {ban_key if data.forwarded_for else data.remote_addr}')
         auth = data.inheaders.get('Authorization')
         nonce_is_stale = False
         log_msg = None
diff --git a/src/calibre/srv/auto_reload.py b/src/calibre/srv/auto_reload.py
index 09f0526b42..d2c7c2ca0d 100644
--- a/src/calibre/srv/auto_reload.py
+++ b/src/calibre/srv/auto_reload.py
@@ -412,7 +412,7 @@ def auto_reload(log, dirs=frozenset(), cmd=None, add_default_dirs=True, listen_o
         cmd.insert(1, 'calibre-server')
     dirs = find_dirs_to_watch(fpath, dirs, add_default_dirs)
     log('Auto-restarting server on changes press Ctrl-C to quit')
-    log('Watching %d directory trees for changes' % len(dirs))
+    log(f'Watching {len(dirs)} directory trees for changes')
     with ReloadServer(listen_on) as server, Worker(cmd, log, server) as worker:
         w = Watcher(dirs, worker, log)
         worker.wakeup = w.wakeup
diff --git a/src/calibre/srv/loop.py b/src/calibre/srv/loop.py
index 64fdb47171..7e5960d4eb 100644
--- a/src/calibre/srv/loop.py
+++ b/src/calibre/srv/loop.py
@@ -773,7 +773,7 @@ class ServerLoop:
         for pool in (self.plugin_pool, self.pool):
             pool.stop(wait_till)
             if pool.workers:
-                self.log.warn('Failed to shutdown %d workers in %s cleanly' % (len(pool.workers), pool.__class__.__name__))
+                self.log.warn(f'Failed to shutdown {len(pool.workers)} workers in {pool.__class__.__name__} cleanly')
         self.jobs_manager.wait_for_shutdown(wait_till)
 
 
diff --git a/src/calibre/srv/manage_users_cli.py b/src/calibre/srv/manage_users_cli.py
index 87c7803201..5e09aa35ba 100644
--- a/src/calibre/srv/manage_users_cli.py
+++ b/src/calibre/srv/manage_users_cli.py
@@ -216,7 +216,7 @@ def manage_users_cli(path=None, args=()):
             prints('%d)' % (i + 1), choice)
         print()
         while True:
-            prompt = question + ' [1-%d]:' % len(choices)
+            prompt = question + f' [1-{len(choices)}]:'
             if default is not None:
                 prompt = question + ' [1-%d %s: %d]' % (
                     len(choices), _('default'), default + 1)
diff --git a/src/calibre/srv/metadata.py b/src/calibre/srv/metadata.py
index 78a80f47d2..93188dd815 100644
--- a/src/calibre/srv/metadata.py
+++ b/src/calibre/srv/metadata.py
@@ -462,7 +462,7 @@ def process_category_node(
         # reflect that in the node structure as well.
         node_data = tag_map.get(id(tag), None)
         if node_data is None:
-            node_id = 'n%d' % len(tag_map)
+            node_id = f'n{len(tag_map)}'
             node_data = items[node_id] = category_item_as_json(tag, clear_rating=clear_rating)
             tag_map[id(tag)] = (node_id, node_data)
             node_to_tag_map[node_id] = tag
diff --git a/src/calibre/srv/routes.py b/src/calibre/srv/routes.py
index 9fcc256cb5..73977bca3b 100644
--- a/src/calibre/srv/routes.py
+++ b/src/calibre/srv/routes.py
@@ -168,7 +168,7 @@ class Route:
         if argspec.args[2:len(self.names)+2] != self.names:
             raise route_error("Function's argument names do not match the variable names in the route")
         if not frozenset(self.type_checkers).issubset(frozenset(self.names)):
-            raise route_error('There exist type checkers that do not correspond to route variables: %r' % (set(self.type_checkers) - set(self.names)))
+            raise route_error(f'There exist type checkers that do not correspond to route variables: {set(self.type_checkers)-set(self.names)!r}')
         self.min_size = found_optional_part if found_optional_part is not False else len(matchers)
         self.max_size = sys.maxsize if self.soak_up_extra else len(matchers)
 
diff --git a/src/calibre/srv/tests/http.py b/src/calibre/srv/tests/http.py
index 880b680b57..ecdd024d34 100644
--- a/src/calibre/srv/tests/http.py
+++ b/src/calibre/srv/tests/http.py
@@ -376,14 +376,14 @@ class TestHTTP(BaseTest):
                 r = conn.getresponse()
                 self.ae(r.status, http_client.PARTIAL_CONTENT)
                 self.ae(str(r.getheader('Accept-Ranges')), 'bytes')
-                self.ae(str(r.getheader('Content-Range')), 'bytes 2-25/%d' % len(fdata))
+                self.ae(str(r.getheader('Content-Range')), f'bytes 2-25/{len(fdata)}')
                 self.ae(int(r.getheader('Content-Length')), 24)
                 self.ae(r.read(), fdata[2:26])
 
                 conn.request('GET', '/test', headers={'Range':'bytes=100000-'})
                 r = conn.getresponse()
                 self.ae(r.status, http_client.REQUESTED_RANGE_NOT_SATISFIABLE)
-                self.ae(str(r.getheader('Content-Range')), 'bytes */%d' % len(fdata))
+                self.ae(str(r.getheader('Content-Range')), f'bytes */{len(fdata)}')
 
                 conn.request('GET', '/test', headers={'Range':'bytes=25-50', 'If-Range':etag})
                 r = conn.getresponse()
diff --git a/src/calibre/utils/fonts/sfnt/cff/writer.py b/src/calibre/utils/fonts/sfnt/cff/writer.py
index 1060c559a1..89b395066a 100644
--- a/src/calibre/utils/fonts/sfnt/cff/writer.py
+++ b/src/calibre/utils/fonts/sfnt/cff/writer.py
@@ -41,7 +41,7 @@ class Index(list):
                 offsets = b''.join(pack(b'>L', x)[1:] for x in offsets)
             else:
                 fmt = {1:'B', 2:'H', 4:'L'}[offsize]
-                offsets = pack(('>%d%s'%(len(offsets), fmt)).encode('ascii'),
+                offsets = pack(f'>{len(offsets)}{fmt}'.encode('ascii'),
                         *offsets)
 
             self.raw = prefix + offsets + obj_data
@@ -102,7 +102,7 @@ class Charsets(list):
     def compile(self):
         ans = pack(b'>B', 0)
         sids = [self.strings(x) for x in self]
-        ans += pack(('>%dH'%len(self)).encode('ascii'), *sids)
+        ans += pack(f'>{len(self)}H'.encode('ascii'), *sids)
         self.raw = ans
         return ans
 
diff --git a/src/calibre/utils/fonts/sfnt/container.py b/src/calibre/utils/fonts/sfnt/container.py
index 3ff8c17b5c..0ef803a259 100644
--- a/src/calibre/utils/fonts/sfnt/container.py
+++ b/src/calibre/utils/fonts/sfnt/container.py
@@ -165,8 +165,7 @@ def test_roundtrip(ff=None):
     if data[:12] != rd[:12]:
         raise ValueError('Roundtripping failed, font header not the same')
     if len(data) != len(rd):
-        raise ValueError('Roundtripping failed, size different (%d vs. %d)'%
-                         (len(data), len(rd)))
+        raise ValueError(f'Roundtripping failed, size different ({len(data)} vs. {len(rd)})')
 
 
 if __name__ == '__main__':
diff --git a/src/calibre/utils/fonts/sfnt/subset.py b/src/calibre/utils/fonts/sfnt/subset.py
index 0ef41c78d0..0b1d64dbec 100644
--- a/src/calibre/utils/fonts/sfnt/subset.py
+++ b/src/calibre/utils/fonts/sfnt/subset.py
@@ -234,7 +234,7 @@ def print_stats(old_stats, new_stats):
         np = nsz/new_total * 100
         suffix = ' | same size'
         if nsz != osz:
-            suffix = ' | reduced to %.1f %%'%(nsz/osz * 100)
+            suffix = f' | reduced to {nsz/osz*100:.1f} %'
         prints('%4s'%table, '  ', '%10s'%osz, '  ', f'{op:5.1f} %', '   ',
                 '%10s'%nsz, '  ', f'{np:5.1f} %', suffix)
     prints('='*80)
@@ -289,7 +289,7 @@ def main(args):
     reduced = (len(sf)/len(orig)) * 100
 
     def sz(x):
-        return '%gKB'%(len(x)/1024.)
+        return f'{len(x)/1024.0:g}KB'
     print_stats(old_stats, new_stats)
     prints('Original size:', sz(orig), 'Subset size:', sz(sf), f'Reduced to: {reduced:g}%')
     prints(f'Subsetting took {taken:g} seconds')
@@ -381,7 +381,7 @@ def all():
             print(name, path, err)
             print()
 
-    print('Average reduction to: %.1f%%'%(sum(averages)/len(averages)))
+    print(f'Average reduction to: {sum(averages)/len(averages):.1f}%')
     print('Total:', total, 'Unsupported:', len(unsupported), 'Failed:',
             len(failed), 'Warnings:', len(warnings))
 
diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py
index 7160ad3486..9882694433 100644
--- a/src/calibre/utils/formatter_functions.py
+++ b/src/calibre/utils/formatter_functions.py
@@ -2172,7 +2172,7 @@ returns the empty string.
         except:
             return ''
         i = d1 - d2
-        return '%.1f'%(i.days + (i.seconds/(24.0*60.0*60.0)))
+        return f'{i.days+(i.seconds/(24.0*60.0*60.0)):.1f}'
 
 
 class BuiltinDateArithmetic(BuiltinFormatterFunction):
diff --git a/src/calibre/utils/smtplib.py b/src/calibre/utils/smtplib.py
index 0c13470a84..1964ab6a87 100644
--- a/src/calibre/utils/smtplib.py
+++ b/src/calibre/utils/smtplib.py
@@ -762,7 +762,7 @@ class SMTP:
             # Hmmm? what's this? -ddm
             # self.esmtp_features['7bit']=""
             if self.has_extn('size'):
-                esmtp_opts.append('size=%d' % len(msg))
+                esmtp_opts.append(f'size={len(msg)}')
             for option in mail_options:
                 esmtp_opts.append(option)
 
@@ -909,7 +909,7 @@ if __name__ == '__main__':
         if not line:
             break
         msg = msg + line
-    print('Message length is %d' % len(msg))
+    print(f'Message length is {len(msg)}')
 
     server = SMTP('localhost')
     server.set_debuglevel(1)
diff --git a/src/calibre/utils/winreg/default_programs.py b/src/calibre/utils/winreg/default_programs.py
index 365b0db708..8ecd29f0da 100644
--- a/src/calibre/utils/winreg/default_programs.py
+++ b/src/calibre/utils/winreg/default_programs.py
@@ -183,7 +183,7 @@ class Register(Thread):
                     prints('Registering with default programs...')
                 register()
                 if DEBUG:
-                    prints('Registered with default programs in %.1f seconds' % (time.monotonic() - st))
+                    prints(f'Registered with default programs in {time.monotonic()-st:.1f} seconds')
 
     def __enter__(self):
         return self
diff --git a/src/calibre/web/feeds/news.py b/src/calibre/web/feeds/news.py
index 88e0ae0ed8..f5b5ada004 100644
--- a/src/calibre/web/feeds/news.py
+++ b/src/calibre/web/feeds/news.py
@@ -1760,7 +1760,7 @@ class BasicNewsRecipe(Recipe):
                 url = url.decode('utf-8')
             if url.startswith('feed://'):
                 url = 'http'+url[4:]
-            self.report_progress(0, _('Fetching feed')+' %s...'%(title if title else url))
+            self.report_progress(0, _('Fetching feed')+f' {title if title else url}...')
             try:
                 purl = urlparse(url, allow_fragments=False)
                 if purl.username or purl.password:
@@ -1780,7 +1780,7 @@ class BasicNewsRecipe(Recipe):
                 ))
             except Exception as err:
                 feed = Feed()
-                msg = 'Failed feed: %s'%(title if title else url)
+                msg = f'Failed feed: {title if title else url}'
                 feed.populate_from_preparsed_feed(msg, [])
                 feed.description = as_unicode(err)
                 parsed_feeds.append(feed)
diff --git a/src/calibre/web/feeds/recipes/model.py b/src/calibre/web/feeds/recipes/model.py
index 2d914c075a..f9ed0b06f6 100644
--- a/src/calibre/web/feeds/recipes/model.py
+++ b/src/calibre/web/feeds/recipes/model.py
@@ -94,7 +94,7 @@ class NewsCategory(NewsTreeItem):
 
     def data(self, role):
         if role == Qt.ItemDataRole.DisplayRole:
-            return (self.cdata + ' [%d]'%len(self.children))
+            return (self.cdata + f' [{len(self.children)}]')
         elif role == Qt.ItemDataRole.FontRole:
             return self.bold_font
         elif role == Qt.ItemDataRole.ForegroundRole and self.category == _('Scheduled'):