From c24998f77e640ca48ed0300860574e44519491b8 Mon Sep 17 00:00:00 2001 From: Arun Persaud Date: Thu, 27 Feb 2025 19:08:52 -0800 Subject: [PATCH 1/3] replaced .format with f-strings --- dodo.py | 2 +- nikola/__main__.py | 24 ++-- nikola/filters.py | 21 ++-- nikola/hierarchy_utils.py | 8 +- nikola/image_processing.py | 10 +- nikola/log.py | 2 +- nikola/nikola.py | 62 +++++----- nikola/packages/datecond/__init__.py | 2 +- nikola/plugin_categories.py | 2 +- nikola/plugins/basic_import.py | 8 +- nikola/plugins/command/auto/__init__.py | 34 +++--- nikola/plugins/command/check.py | 34 +++--- nikola/plugins/command/deploy.py | 12 +- nikola/plugins/command/github_deploy.py | 18 +-- nikola/plugins/command/import_wordpress.py | 114 +++++++++---------- nikola/plugins/command/init.py | 20 ++-- nikola/plugins/command/new_post.py | 39 ++++--- nikola/plugins/command/plugin.py | 16 +-- nikola/plugins/command/serve.py | 14 +-- nikola/plugins/command/status.py | 14 +-- nikola/plugins/command/subtheme.py | 11 +- nikola/plugins/command/theme.py | 44 +++---- nikola/plugins/command/version.py | 6 +- nikola/plugins/compile/ipynb.py | 6 +- nikola/plugins/compile/markdown/mdx_gist.py | 5 +- nikola/plugins/compile/pandoc.py | 4 +- nikola/plugins/compile/php.py | 2 +- nikola/plugins/compile/rest/doc.py | 14 +-- nikola/plugins/compile/rest/gist.py | 12 +- nikola/plugins/compile/rest/listing.py | 5 +- nikola/plugins/compile/rest/media.py | 2 +- nikola/plugins/compile/rest/vimeo.py | 4 +- nikola/plugins/misc/scan_posts.py | 2 +- nikola/plugins/misc/taxonomies_classifier.py | 5 +- nikola/plugins/shortcode/chart.py | 4 +- nikola/plugins/shortcode/emoji/__init__.py | 6 +- nikola/plugins/shortcode/gist.py | 16 +-- nikola/plugins/shortcode/listing.py | 3 +- nikola/plugins/shortcode/thumbnail.py | 14 +-- nikola/plugins/task/archive.py | 4 +- nikola/plugins/task/galleries.py | 20 ++-- nikola/plugins/task/gzip.py | 2 +- nikola/plugins/task/listings.py | 8 +- nikola/plugins/task/robots.py | 4 +- nikola/plugins/task/sitemap.py | 2 +- nikola/plugins/task/taxonomies.py | 8 +- nikola/post.py | 43 ++++--- nikola/shortcodes.py | 34 +++--- nikola/utils.py | 72 ++++++------ scripts/import_po.py | 4 +- scripts/jinjify.py | 10 +- scripts/langstatus.py | 6 +- tests/integration/helper.py | 10 +- tests/test_metadata_extractors.py | 24 ++-- tests/test_rst_compiler.py | 2 +- tests/test_shortcodes.py | 4 +- 56 files changed, 425 insertions(+), 453 deletions(-) diff --git a/dodo.py b/dodo.py index 1d78634989..54157198e7 100644 --- a/dodo.py +++ b/dodo.py @@ -53,5 +53,5 @@ def task_gen_completion(): yield { 'name': shell, 'actions': [cmd.format(shell)], - 'targets': ['_nikola_{0}'.format(shell)], + 'targets': [f'_nikola_{shell}'], } diff --git a/nikola/__main__.py b/nikola/__main__.py index 7ff5ce28c9..30167e011a 100644 --- a/nikola/__main__.py +++ b/nikola/__main__.py @@ -139,15 +139,15 @@ def main(args=None): except Exception: if os.path.exists(conf_filename): msg = traceback.format_exc() - LOGGER.error('"{0}" cannot be parsed.\n{1}'.format(conf_filename, msg)) + LOGGER.error(f'"{conf_filename}" cannot be parsed.\n{msg}') return 1 elif needs_config_file and conf_filename_changed: - LOGGER.error('Cannot find configuration file "{0}".'.format(conf_filename)) + LOGGER.error(f'Cannot find configuration file "{conf_filename}".') return 1 config = {} if conf_filename_changed: - LOGGER.info("Using config file '{0}'".format(conf_filename)) + LOGGER.info(f"Using config file '{conf_filename}'") if config: if os.path.isdir('plugins') and not os.path.exists('plugins/__init__.py'): @@ -186,7 +186,7 @@ def print_usage(cmds): print("Available commands:") for cmd_name in sorted(cmds.keys()): cmd = cmds[cmd_name] - print(" nikola {:20s} {}".format(cmd_name, cmd.doc_purpose)) + print(f" nikola {cmd_name:20s} {cmd.doc_purpose}") print("") print(" nikola help show help / reference") print(" nikola help show command usage") @@ -359,7 +359,7 @@ def run(self, cmd_args): cmd_args = ['version'] args = ['version'] if args[0] not in sub_cmds.keys(): - LOGGER.error("Unknown command {0}".format(args[0])) + LOGGER.error(f"Unknown command {args[0]}") sugg = defaultdict(list) sub_filtered = (i for i in sub_cmds.keys() if i != 'run') for c in sub_filtered: @@ -368,7 +368,7 @@ def run(self, cmd_args): if sugg.keys(): best_sugg = sugg[min(sugg.keys())] if len(best_sugg) == 1: - LOGGER.info('Did you mean "{}"?'.format(best_sugg[0])) + LOGGER.info(f'Did you mean "{best_sugg[0]}"?') else: LOGGER.info('Did you mean "{}" or "{}"?'.format('", "'.join(best_sugg[:-1]), best_sugg[-1])) return 3 @@ -400,7 +400,7 @@ def _command_help(self: Command): """Return help text for a command.""" text = [] - usage = "{} {} {}".format(self.bin_name, self.name, self.doc_usage) + usage = f"{self.bin_name} {self.name} {self.doc_usage}" text.extend(textwrap.wrap(usage, subsequent_indent=' ')) text.extend(_wrap(self.doc_purpose, 4)) @@ -410,7 +410,7 @@ def _command_help(self: Command): options[opt.section].append(opt) for section, opts in sorted(options.items()): if section: - section_name = '\n{}'.format(section) + section_name = f'\n{section}' text.extend(_wrap(section_name, 2)) for opt in opts: # ignore option that cant be modified on cmd line @@ -421,15 +421,15 @@ def _command_help(self: Command): if '%(default)s' in opt_help: opt_help = opt.help % {'default': opt.default} elif opt.default != '' and opt.default is not False and opt.default is not None: - opt_help += ' [default: {}]'.format(opt.default) + opt_help += f' [default: {opt.default}]' opt_choices = opt.help_choices() - desc = '{} {}'.format(opt_help, opt_choices) + desc = f'{opt_help} {opt_choices}' text.extend(_wrap(desc, 8)) # print bool inverse option if opt.inverse: - text.extend(_wrap('--{}'.format(opt.inverse), 4)) - text.extend(_wrap('opposite of --{}'.format(opt.long), 8)) + text.extend(_wrap(f'--{opt.inverse}', 4)) + text.extend(_wrap(f'opposite of --{opt.long}', 8)) if self.doc_description is not None: text.append("\n\nDescription:") diff --git a/nikola/filters.py b/nikola/filters.py index 576efad656..9fd8d2b025 100644 --- a/nikola/filters.py +++ b/nikola/filters.py @@ -168,36 +168,33 @@ def yui_compressor(infile, executable=None): raise Exception('yui-compressor is not installed.') return False - return runinplace('{} --nomunge %1 -o %2'.format(yuicompressor), infile) + return runinplace(f'{yuicompressor} --nomunge %1 -o %2', infile) @_ConfigurableFilter(executable='CLOSURE_COMPILER_EXECUTABLE') def closure_compiler(infile, executable='closure-compiler'): """Run closure-compiler on a file.""" return runinplace( - '{} --warning_level QUIET --js %1 --js_output_file %2'.format(executable), - infile, + f'{executable} --warning_level QUIET --js %1 --js_output_file %2', infile ) @_ConfigurableFilter(executable='OPTIPNG_EXECUTABLE') def optipng(infile, executable='optipng'): """Run optipng on a file.""" - return runinplace('{} -preserve -o2 -quiet %1'.format(executable), infile) + return runinplace(f'{executable} -preserve -o2 -quiet %1', infile) @_ConfigurableFilter(executable='JPEGOPTIM_EXECUTABLE') def jpegoptim(infile, executable='jpegoptim'): """Run jpegoptim on a file.""" - return runinplace('{} -p --strip-all -q %1'.format(executable), infile) + return runinplace(f'{executable} -p --strip-all -q %1', infile) @_ConfigurableFilter(executable='JPEGOPTIM_EXECUTABLE') def jpegoptim_progressive(infile, executable='jpegoptim'): """Run jpegoptim on a file and convert to progressive.""" - return runinplace( - '{} -p --strip-all --all-progressive -q %1'.format(executable), infile - ) + return runinplace(f'{executable} -p --strip-all --all-progressive -q %1', infile) @_ConfigurableFilter(executable='HTML_TIDY_EXECUTABLE') @@ -532,9 +529,7 @@ def add_header_permalinks(fname, xpath_list=None, file_blacklist=None): hid = node.attrib['id'] new_node = lxml.html.fragment_fromstring( - ''.format( - hid - ) + f'' ) node.append(new_node) @@ -563,7 +558,7 @@ def deduplicate_ids(data, top_classes=None): # Well, that sucks. for i in duplicated_ids: # Results are ordered the same way they are ordered in document - offending_elements = doc.xpath('//*[@id="{}"]'.format(i)) + offending_elements = doc.xpath(f'//*[@id="{i}"]') counter = 2 # If this is a story or a post, do it from top to bottom, because # updates to those are more likely to appear at the bottom of pages. @@ -578,7 +573,7 @@ def deduplicate_ids(data, top_classes=None): for e in off: new_id = i while new_id in seen_ids: - new_id = '{0}-{1}'.format(i, counter) + new_id = f'{i}-{counter}' counter += 1 e.attrib['id'] = new_id seen_ids.add(new_id) diff --git a/nikola/hierarchy_utils.py b/nikola/hierarchy_utils.py index f001b59254..1356e63d55 100644 --- a/nikola/hierarchy_utils.py +++ b/nikola/hierarchy_utils.py @@ -96,13 +96,13 @@ def __str__(self): def _repr_partial(self): """Return partial representation.""" if self.parent: - return "{0}/{1!r}".format(self.parent._repr_partial(), self.name) + return f"{self.parent._repr_partial()}/{self.name!r}" else: return repr(self.name) def __repr__(self): """Return programmer-friendly node representation.""" - return "".format(self._repr_partial()) + return f"" def clone_treenode(treenode, parent=None, acceptor=lambda x: True): @@ -188,10 +188,10 @@ def parse_escaped_hierarchical_category_name(category_name): next_slash = category_name.find('/', index) else: if len(category_name) == next_backslash + 1: - raise Exception("Unexpected '\\' in '{0}' at last position!".format(category_name)) + raise Exception(f"Unexpected '\\' in '{category_name}' at last position!") esc_ch = category_name[next_backslash + 1] if esc_ch not in {'/', '\\'}: - raise Exception("Unknown escape sequence '\\{0}' in '{1}'!".format(esc_ch, category_name)) + raise Exception(f"Unknown escape sequence '\\{esc_ch}' in '{category_name}'!") current = (current if current else "") + category_name[index:next_backslash] + esc_ch index = next_backslash + 2 next_backslash = category_name.find('\\', index) diff --git a/nikola/image_processing.py b/nikola/image_processing.py index ab0341d505..8c9d59d9a3 100644 --- a/nikola/image_processing.py +++ b/nikola/image_processing.py @@ -106,7 +106,7 @@ def resize_image(self, src, dst=None, max_size=None, bigger_panoramas=True, pres if max_sizes is None: max_sizes = [max_size] if len(max_sizes) != len(dst_paths): - raise ValueError('resize_image called with incompatible arguments: {} / {}'.format(dst_paths, max_sizes)) + raise ValueError(f'resize_image called with incompatible arguments: {dst_paths} / {max_sizes}') extension = os.path.splitext(src)[1].lower() if extension in {'.svg', '.svgz'}: self.resize_svg(src, dst_paths, max_sizes, bigger_panoramas) @@ -169,8 +169,8 @@ def resize_image(self, src, dst=None, max_size=None, bigger_panoramas=True, pres im.save(dst, **save_args) except Exception as e: - self.logger.warning("Can't process {0}, using original " - "image! ({1})".format(src, e)) + self.logger.warning(f"Can't process {src}, using original " + f"image! ({e})") utils.copy_file(src, dst) def resize_svg(self, src, dst_paths, max_sizes, bigger_panoramas): @@ -217,8 +217,8 @@ def resize_svg(self, src, dst_paths, max_sizes, bigger_panoramas): self.logger.warning("No width/height in %s. Original exception: %s" % (src, e)) utils.copy_file(src, dst) except Exception as e: - self.logger.warning("Can't process {0}, using original " - "image! ({1})".format(src, e)) + self.logger.warning(f"Can't process {src}, using original " + f"image! ({e})") utils.copy_file(src, dst) def image_date(self, src): diff --git a/nikola/log.py b/nikola/log.py index b09b766c8a..0316076a70 100644 --- a/nikola/log.py +++ b/nikola/log.py @@ -178,7 +178,7 @@ def showwarning(message, category, filename, lineno, file=None, line=None): n = category.__name__ except AttributeError: n = str(category) - get_logger(n).warning("{0}:{1}: {2}".format(filename, lineno, message)) + get_logger(n).warning(f"{filename}:{lineno}: {message}") warnings.showwarning = showwarning diff --git a/nikola/nikola.py b/nikola/nikola.py index 6f80377c1f..0f963bfd2e 100644 --- a/nikola/nikola.py +++ b/nikola/nikola.py @@ -358,7 +358,7 @@ def _enclosure(post, lang): except KeyError: length = 0 except ValueError: - utils.LOGGER.warning("Invalid enclosure length for post {0}".format(post.source_path)) + utils.LOGGER.warning(f"Invalid enclosure length for post {post.source_path}") length = 0 url = enclosure mime = mimetypes.guess_type(url)[0] @@ -883,7 +883,7 @@ def __init__(self, **config) -> None: if missing_plugins: utils.LOGGER.warning('The "{}" plugin was replaced by several taxonomy plugins (see PR #2535): {}'.format(old_plugin_name, ', '.join(new_plugin_names))) utils.LOGGER.warning('You are currently disabling "{}", but not the following new taxonomy plugins: {}'.format(old_plugin_name, ', '.join(missing_plugins))) - utils.LOGGER.warning('Please also disable these new plugins or remove "{}" from the DISABLED_PLUGINS list.'.format(old_plugin_name)) + utils.LOGGER.warning(f'Please also disable these new plugins or remove "{old_plugin_name}" from the DISABLED_PLUGINS list.') self.config['DISABLED_PLUGINS'].extend(missing_plugins) # Special-case logic for "render_indexes" to fix #2591 @@ -941,8 +941,7 @@ def __init__(self, **config) -> None: # Load built-in metadata extractors metadata_extractors.load_defaults(self, self.metadata_extractors_by) if metadata_extractors.DEFAULT_EXTRACTOR is None: - utils.LOGGER.error("Could not find default meta extractor ({})".format( - metadata_extractors.DEFAULT_EXTRACTOR_NAME)) + utils.LOGGER.error(f"Could not find default meta extractor ({metadata_extractors.DEFAULT_EXTRACTOR_NAME})") sys.exit(1) # The Pelican metadata format requires a markdown extension @@ -1011,7 +1010,7 @@ def plugin_position_in_places(plugin: PluginInfo): return i except ValueError: pass - utils.LOGGER.warning("Duplicate plugin found in unexpected location: {}".format(plugin.source_dir)) + utils.LOGGER.warning(f"Duplicate plugin found in unexpected location: {plugin.source_dir}") return len(self._plugin_places) plugin_dict = defaultdict(list) @@ -1022,8 +1021,7 @@ def plugin_position_in_places(plugin: PluginInfo): if len(plugins) > 1: # Sort by locality plugins.sort(key=plugin_position_in_places) - utils.LOGGER.debug("Plugin {} exists in multiple places, using {}".format( - name, plugins[-1].source_dir)) + utils.LOGGER.debug(f"Plugin {name} exists in multiple places, using {plugins[-1].source_dir}") result.append(plugins[-1]) return result @@ -1096,7 +1094,7 @@ def init_plugins(self, commands_only=False, load_all=False) -> None: file_extensions.update(exts) else: # Stop scanning for more: once we get None, we have to load all compilers anyway - utils.LOGGER.debug("Post scanner {0!r} does not implement `supported_extensions`, loading all compilers".format(post_scanner)) + utils.LOGGER.debug(f"Post scanner {post_scanner!r} does not implement `supported_extensions`, loading all compilers") file_extensions = None break to_add = [] @@ -1133,7 +1131,7 @@ def init_plugins(self, commands_only=False, load_all=False) -> None: if not taxonomy.is_enabled(): continue if taxonomy.classification_name in self.taxonomy_plugins: - utils.LOGGER.error("Found more than one taxonomy with classification name '{}'!".format(taxonomy.classification_name)) + utils.LOGGER.error(f"Found more than one taxonomy with classification name '{taxonomy.classification_name}'!") sys.exit(1) self.taxonomy_plugins[taxonomy.classification_name] = taxonomy @@ -1364,7 +1362,7 @@ def _get_messages(self): themes_dirs=self.themes_dirs) return self._MESSAGES except utils.LanguageNotFoundError as e: - utils.LOGGER.error('''Cannot load language "{0}". Please make sure it is supported by Nikola itself, or that you have the appropriate messages files in your themes.'''.format(e.lang)) + utils.LOGGER.error(f'''Cannot load language "{e.lang}". Please make sure it is supported by Nikola itself, or that you have the appropriate messages files in your themes.''') sys.exit(1) MESSAGES = property(_get_messages) @@ -1395,8 +1393,8 @@ def _get_template_system(self): template_sys_name = utils.get_template_engine(self.THEMES) pi = self.plugin_manager.get_plugin_by_name(template_sys_name, "TemplateSystem") if pi is None: - sys.stderr.write("Error loading {0} template system " - "plugin\n".format(template_sys_name)) + sys.stderr.write(f"Error loading {template_sys_name} template system " + "plugin\n") sys.exit(1) self._template_system = typing.cast(TemplateSystem, pi.plugin_object) @@ -1438,15 +1436,15 @@ def get_compiler(self, source_name): langs = langs[:1] else: sys.exit("COMPILERS in conf.py does not tell me how to " - "handle '{0}' extensions.".format(ext)) + f"handle '{ext}' extensions.") lang = langs[0] try: compiler = self.compilers[lang] except KeyError: - sys.exit("Cannot find '{0}' compiler; " + sys.exit(f"Cannot find '{lang}' compiler; " "it might require an extra plugin -- " - "do you have it installed?".format(lang)) + "do you have it installed?") self.inverse_compilers[ext] = compiler return compiler @@ -1660,7 +1658,7 @@ def url_replacer(self, src, dst, lang=None, url_type=None): result += "#" + parsed_dst.fragment if not result: - raise ValueError("Failed to parse link: {0}".format((src, dst, i, src_elems, dst_elems))) + raise ValueError(f"Failed to parse link: {(src, dst, i, src_elems, dst_elems)}") return result @@ -1906,7 +1904,7 @@ def path(self, kind, name, lang=None, is_link=False, **kwargs): try: path = self.path_handlers[kind](name, lang, **kwargs) except KeyError: - utils.LOGGER.warning("Unknown path request of kind: {0}".format(kind)) + utils.LOGGER.warning(f"Unknown path request of kind: {kind}") return "" # If path handler returns a string we consider it to be an absolute URL not requiring any @@ -1965,10 +1963,10 @@ def slug_path(self, name, lang): """ results = [p for p in self.timeline if p.meta('slug') == name] if not results: - utils.LOGGER.warning("Cannot resolve path request for slug: {0}".format(name)) + utils.LOGGER.warning(f"Cannot resolve path request for slug: {name}") else: if len(results) > 1: - utils.LOGGER.warning('Ambiguous path request for slug: {0}'.format(name)) + utils.LOGGER.warning(f'Ambiguous path request for slug: {name}') return [_f for _f in results[0].permalink(lang).split('/')] def slug_source(self, name, lang): @@ -1995,16 +1993,16 @@ def filename_path(self, name, lang): """ results = [p for p in self.timeline if p.source_path == name] if not results: - utils.LOGGER.warning("Cannot resolve path request for filename: {0}".format(name)) + utils.LOGGER.warning(f"Cannot resolve path request for filename: {name}") else: if len(results) > 1: - utils.LOGGER.error("Ambiguous path request for filename: {0}".format(name)) + utils.LOGGER.error(f"Ambiguous path request for filename: {name}") return [_f for _f in results[0].permalink(lang).split('/') if _f] def register_path_handler(self, kind, f): """Register a path handler.""" if kind in self.path_handlers: - utils.LOGGER.warning('Conflicting path handlers for kind: {0}'.format(kind)) + utils.LOGGER.warning(f'Conflicting path handlers for kind: {kind}') else: self.path_handlers[kind] = f @@ -2062,7 +2060,7 @@ def register_filter(self, filter_name, filter_definition): one argument (the filename). """ if filter_name in self.filters: - utils.LOGGER.warning('''The filter "{0}" is defined more than once.'''.format(filter_name)) + utils.LOGGER.warning(f'''The filter "{filter_name}" is defined more than once.''') self.filters[filter_name] = filter_definition def file_exists(self, path, not_empty=False): @@ -2094,7 +2092,7 @@ def flatten(task): for pluginInfo in self.plugin_manager.get_plugins_of_category(plugin_category): for task in flatten(pluginInfo.plugin_object.gen_tasks()): if 'basename' not in task: - raise ValueError("Task {0} does not have a basename".format(task)) + raise ValueError(f"Task {task} does not have a basename") task = self.clean_task_paths(task) if 'task_dep' not in task: task['task_dep'] = [] @@ -2106,7 +2104,7 @@ def flatten(task): flag = True yield self.clean_task_paths(task) if flag: - task_dep.append('{0}_{1}'.format(name, multi.plugin_object.name)) + task_dep.append(f'{name}_{multi.plugin_object.name}') if pluginInfo.plugin_object.is_default: task_dep.append(pluginInfo.plugin_object.name) yield { @@ -2226,7 +2224,7 @@ def scan_posts(self, really=False, ignore_quit=False, quiet=False): self.posts.append(post) self.posts_per_year[str(post.date.year)].append(post) self.posts_per_month[ - '{0}/{1:02d}'.format(post.date.year, post.date.month)].append(post) + f'{post.date.year}/{post.date.month:02d}'].append(post) for lang in self.config['TRANSLATIONS'].keys(): for tag in post.tags_for_language(lang): _tag_slugified = utils.slugify(tag, lang) @@ -2247,16 +2245,10 @@ def scan_posts(self, really=False, ignore_quit=False, quiet=False): src_dest = post.destination_path(lang=lang, extension=post.source_ext()) src_file = post.translated_source_path(lang=lang) if dest in self.post_per_file: - utils.LOGGER.error('Two posts are trying to generate {0}: {1} and {2}'.format( - dest, - self.post_per_file[dest].source_path, - post.source_path)) + utils.LOGGER.error(f'Two posts are trying to generate {dest}: {self.post_per_file[dest].source_path} and {post.source_path}') quit = True if (src_dest in self.post_per_file) and self.config['COPY_SOURCES']: - utils.LOGGER.error('Two posts are trying to generate {0}: {1} and {2}'.format( - src_dest, - self.post_per_file[dest].source_path, - post.source_path)) + utils.LOGGER.error(f'Two posts are trying to generate {src_dest}: {self.post_per_file[dest].source_path} and {post.source_path}') quit = True self.post_per_file[dest] = post self.post_per_file[src_dest] = post @@ -2322,7 +2314,7 @@ def generic_renderer(self, lang, output_name, template_name, filters, file_deps= deps_dict.update(post_deps_dict) for k, v in self.GLOBAL_CONTEXT['template_hooks'].items(): - deps_dict['||template_hooks|{0}||'.format(k)] = v.calculate_deps() + deps_dict[f'||template_hooks|{k}||'] = v.calculate_deps() for k in self._GLOBAL_CONTEXT_TRANSLATABLE: deps_dict[k] = deps_dict['global'][k](lang) diff --git a/nikola/packages/datecond/__init__.py b/nikola/packages/datecond/__init__.py index b7a71c8c60..394b256814 100644 --- a/nikola/packages/datecond/__init__.py +++ b/nikola/packages/datecond/__init__.py @@ -91,6 +91,6 @@ def date_in_range(date_range, date, debug=False, now=None): left = date right = dateutil.parser.parse(value) if debug: # pragma: no cover - print(" <{0} {1} {2}>".format(left, comparison_operator, right)) + print(f" <{left} {comparison_operator} {right}>") out = out and OPERATORS[comparison_operator](left, right) return out diff --git a/nikola/plugin_categories.py b/nikola/plugin_categories.py index 140f12779d..b92d869559 100644 --- a/nikola/plugin_categories.py +++ b/nikola/plugin_categories.py @@ -453,7 +453,7 @@ def check_requirements(self): try: __import__(import_name) except ImportError: - req_missing([pip_name], "use {0} metadata".format(friendly_name), python=True, optional=False) + req_missing([pip_name], f"use {friendly_name} metadata", python=True, optional=False) class SignalHandler(BasePlugin): diff --git a/nikola/plugins/basic_import.py b/nikola/plugins/basic_import.py index 59fb053411..f73ea14781 100644 --- a/nikola/plugins/basic_import.py +++ b/nikola/plugins/basic_import.py @@ -83,7 +83,7 @@ def configure_redirections(url_map, base_dir=''): src = (urlparse(k).path + 'index.html')[1:] dst = (urlparse(v).path) if src == index: - utils.LOGGER.warning("Can't do a redirect for: {0!r}".format(k)) + utils.LOGGER.warning(f"Can't do a redirect for: {k!r}") else: redirections.append((src, dst)) return redirections @@ -94,8 +94,8 @@ def generate_base_site(self): os.system('nikola init -q ' + self.output_folder) else: self.import_into_existing_site = True - utils.LOGGER.warning('The folder {0} already exists - assuming that this is a ' - 'already existing Nikola site.'.format(self.output_folder)) + utils.LOGGER.warning(f'The folder {self.output_folder} already exists - assuming that this is a ' + 'already existing Nikola site.') filename = utils.pkg_resources_path('nikola', 'conf.py.in') # The 'strict_undefined=True' will give the missing symbol name if any, @@ -181,7 +181,7 @@ def get_configuration_output_path(self): time=datetime.datetime.now().strftime('%Y%m%d_%H%M%S'), name=self.name) config_output_path = os.path.join(self.output_folder, filename) - utils.LOGGER.info('Configuration will be written to: {0}'.format(config_output_path)) + utils.LOGGER.info(f'Configuration will be written to: {config_output_path}') return config_output_path diff --git a/nikola/plugins/command/auto/__init__.py b/nikola/plugins/command/auto/__init__.py index 7c702214c9..4c6983eecb 100644 --- a/nikola/plugins/command/auto/__init__.py +++ b/nikola/plugins/command/auto/__init__.py @@ -196,11 +196,11 @@ def _execute(self, options, args): '--backend={}'.format(options['backend'])] port = options and options.get('port') - self.snippet = ''' - '''.format(port) + ''' # Deduplicate entries by using a set -- otherwise, multiple rebuilds are triggered watched = set([ @@ -287,10 +287,10 @@ def _execute(self, options, args): return if options['ipv6'] or '::' in host: - server_url = "http://[{0}]:{1}/".format(host, port) + server_url = f"http://[{host}]:{port}/" else: - server_url = "http://{0}:{1}/".format(host, port) - self.logger.info("Serving on {0} ...".format(server_url)) + server_url = f"http://{host}:{port}/" + self.logger.info(f"Serving on {server_url} ...") if browser: # Some browsers fail to load 0.0.0.0 (Issue #2755) @@ -299,7 +299,7 @@ def _execute(self, options, args): else: # server_url always ends with a "/": browser_url = "{0}{1}".format(server_url, base_path.lstrip("/")) - self.logger.info("Opening {0} in the default web browser...".format(browser_url)) + self.logger.info(f"Opening {browser_url} in the default web browser...") webbrowser.open(browser_url) # Run the event loop forever and handle shutdowns. @@ -364,7 +364,7 @@ async def queue_rebuild(self, event) -> None: event.is_directory): # Skip on folders, these are usually duplicates return - self.logger.debug('Queuing rebuild from {0}'.format(event_path)) + self.logger.debug(f'Queuing rebuild from {event_path}') await self.rebuild_queue.put((datetime.datetime.now(), event_path)) async def run_rebuild_queue(self) -> None: @@ -372,7 +372,7 @@ async def run_rebuild_queue(self) -> None: while True: date, event_path = await self.rebuild_queue.get() if date < (self.last_rebuild + self.delta_last_rebuild): - self.logger.debug("Skipping rebuild from {0} (within delta)".format(event_path)) + self.logger.debug(f"Skipping rebuild from {event_path} (within delta)") continue await self._rebuild_site(event_path) @@ -381,7 +381,7 @@ async def _rebuild_site(self, event_path: typing.Optional[str] = None) -> None: self.is_rebuilding = True self.last_rebuild = datetime.datetime.now() if event_path: - self.logger.info('REBUILDING SITE (from {0})'.format(event_path)) + self.logger.info(f'REBUILDING SITE (from {event_path})') else: self.logger.info('REBUILDING SITE') @@ -401,7 +401,7 @@ async def run_reload_queue(self) -> None: """Send reloads from a queue to limit CPU usage.""" while True: p = await self.reload_queue.get() - self.logger.info('REFRESHING: {0}'.format(p)) + self.logger.info(f'REFRESHING: {p}') await self._send_reload_command(p) if self.is_rebuilding: await asyncio.sleep(REBUILDING_REFRESH_DELAY) @@ -440,7 +440,7 @@ async def websocket_handler(self, request): while True: msg = await ws.receive() - self.logger.debug("Received message: {0}".format(msg)) + self.logger.debug(f"Received message: {msg}") if msg.type == aiohttp.WSMsgType.TEXT: message = msg.json() if message['command'] == 'hello': @@ -453,7 +453,7 @@ async def websocket_handler(self, request): } await ws.send_json(response) elif message['command'] != 'info': - self.logger.warning("Unknown command in message: {0}".format(message)) + self.logger.warning(f"Unknown command in message: {message}") elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.CLOSING): break elif msg.type == aiohttp.WSMsgType.CLOSE: @@ -461,13 +461,13 @@ async def websocket_handler(self, request): await ws.close() break elif msg.type == aiohttp.WSMsgType.ERROR: - self.logger.error('WebSocket connection closed with exception {0}'.format(ws.exception())) + self.logger.error(f'WebSocket connection closed with exception {ws.exception()}') break else: - self.logger.warning("Received unknown message: {0}".format(msg)) + self.logger.warning(f"Received unknown message: {msg}") self.sockets.remove(ws) - self.logger.debug("WebSocket connection closed: {0}".format(ws)) + self.logger.debug(f"WebSocket connection closed: {ws}") return ws @@ -492,7 +492,7 @@ async def send_to_websockets(self, message: dict) -> None: to_delete.append(ws) except RuntimeError as e: if 'closed' in e.args[0]: - self.logger.warning("WebSocket {0} closed uncleanly".format(ws)) + self.logger.warning(f"WebSocket {ws} closed uncleanly") to_delete.append(ws) else: raise diff --git a/nikola/plugins/command/check.py b/nikola/plugins/command/check.py index 3f61dd2990..5f03e4fe42 100644 --- a/nikola/plugins/command/check.py +++ b/nikola/plugins/command/check.py @@ -215,7 +215,7 @@ def analyze(self, fname, find_sources=False, check_remote=False, ignore_query_st # Do not look at links in the cache, which are not parsed by # anyone and may result in false positives. Problems arise # with galleries, for example. Full rationale: (Issue #1447) - self.logger.warning("Ignoring {0} (in cache, links may be incorrect)".format(filename)) + self.logger.warning(f"Ignoring {filename} (in cache, links may be incorrect)") return False if not os.path.exists(fname): @@ -257,7 +257,7 @@ def analyze(self, fname, find_sources=False, check_remote=False, ignore_query_st target = urldefrag(target)[0] if any([urlparse(target).netloc.endswith(_) for _ in ['example.com', 'example.net', 'example.org']]): - self.logger.debug("Not testing example address \"{0}\".".format(target)) + self.logger.debug(f"Not testing example address \"{target}\".") continue # absolute URL to root-relative @@ -268,13 +268,13 @@ def analyze(self, fname, find_sources=False, check_remote=False, ignore_query_st # Warn about links from https to http (mixed-security) if base_url.netloc == parsed.netloc and base_url.scheme == "https" and parsed.scheme == "http": - self.logger.warning("Mixed-content security for link in {0}: {1}".format(filename, target)) + self.logger.warning(f"Mixed-content security for link in {filename}: {target}") # Link to an internal REDIRECTIONS page if target in self.internal_redirects: redir_status_code = 301 redir_target = [_dest for _target, _dest in self.site.config['REDIRECTIONS'] if urljoin('/', _target) == target][0] - self.logger.warning("Remote link moved PERMANENTLY to \"{0}\" and should be updated in {1}: {2} [HTTP: 301]".format(redir_target, filename, target)) + self.logger.warning(f"Remote link moved PERMANENTLY to \"{redir_target}\" and should be updated in {filename}: {target} [HTTP: 301]") # Absolute links to other domains, skip # Absolute links when using only paths, skip. @@ -284,11 +284,11 @@ def analyze(self, fname, find_sources=False, check_remote=False, ignore_query_st continue if target in self.checked_remote_targets: # already checked this exact target if self.checked_remote_targets[target] in [301, 308]: - self.logger.warning("Remote link PERMANENTLY redirected in {0}: {1} [Error {2}]".format(filename, target, self.checked_remote_targets[target])) + self.logger.warning(f"Remote link PERMANENTLY redirected in {filename}: {target} [Error {self.checked_remote_targets[target]}]") elif self.checked_remote_targets[target] in [302, 307]: - self.logger.debug("Remote link temporarily redirected in {0}: {1} [HTTP: {2}]".format(filename, target, self.checked_remote_targets[target])) + self.logger.debug(f"Remote link temporarily redirected in {filename}: {target} [HTTP: {self.checked_remote_targets[target]}]") elif self.checked_remote_targets[target] > 399: - self.logger.error("Broken link in {0}: {1} [Error {2}]".format(filename, target, self.checked_remote_targets[target])) + self.logger.error(f"Broken link in {filename}: {target} [Error {self.checked_remote_targets[target]}]") continue # Skip whitelisted targets @@ -312,21 +312,21 @@ def analyze(self, fname, find_sources=False, check_remote=False, ignore_query_st resp = requests.get(target, headers=req_headers, allow_redirects=True, timeout=self.timeout) # Permanent redirects should be updated if redir_status_code in [301, 308]: - self.logger.warning("Remote link moved PERMANENTLY to \"{0}\" and should be updated in {1}: {2} [HTTP: {3}]".format(resp.url, filename, target, redir_status_code)) + self.logger.warning(f"Remote link moved PERMANENTLY to \"{resp.url}\" and should be updated in {filename}: {target} [HTTP: {redir_status_code}]") if redir_status_code in [302, 307]: - self.logger.debug("Remote link temporarily redirected to \"{0}\" in {1}: {2} [HTTP: {3}]".format(resp.url, filename, target, redir_status_code)) + self.logger.debug(f"Remote link temporarily redirected to \"{resp.url}\" in {filename}: {target} [HTTP: {redir_status_code}]") self.checked_remote_targets[resp.url] = resp.status_code self.checked_remote_targets[target] = redir_status_code else: self.checked_remote_targets[target] = resp.status_code if resp.status_code > 399: # Error - self.logger.error("Broken link in {0}: {1} [Error {2}]".format(filename, target, resp.status_code)) + self.logger.error(f"Broken link in {filename}: {target} [Error {resp.status_code}]") continue elif resp.status_code <= 399: # The address leads *somewhere* that is not an error - self.logger.debug("Successfully checked remote link in {0}: {1} [HTTP: {2}]".format(filename, target, resp.status_code)) + self.logger.debug(f"Successfully checked remote link in {filename}: {target} [HTTP: {resp.status_code}]") continue - self.logger.warning("Could not check remote link in {0}: {1} [Unknown problem]".format(filename, target)) + self.logger.warning(f"Could not check remote link in {filename}: {target} [Unknown problem]") continue if url_type == 'rel_path': @@ -380,17 +380,17 @@ def analyze(self, fname, find_sources=False, check_remote=False, ignore_query_st elif target_filename not in self.existing_targets: if os.path.exists(target_filename): - self.logger.info("Good link {0} => {1}".format(target, target_filename)) + self.logger.info(f"Good link {target} => {target_filename}") self.existing_targets.add(target_filename) else: rv = True - self.logger.warning("Broken link in {0}: {1}".format(filename, target)) + self.logger.warning(f"Broken link in {filename}: {target}") if find_sources: self.logger.warning("Possible sources:") self.logger.warning("\n".join(deps[filename])) self.logger.warning("===============================\n") except Exception as exc: - self.logger.error(u"Error with: {0} {1}".format(filename, exc)) + self.logger.error(f"Error with: {filename} {exc}") return rv def scan_links(self, find_sources=False, check_remote=False, ignore_query_strings=False): @@ -451,7 +451,7 @@ def clean_files(self): """Remove orphaned files.""" only_on_output, _ = real_scan_files(self.site, self.cache) for f in only_on_output: - self.logger.debug('removed: {0}'.format(f)) + self.logger.debug(f'removed: {f}') os.unlink(f) warn_flag = bool(only_on_output) @@ -465,7 +465,7 @@ def clean_files(self): for d in all_dirs: try: os.rmdir(d) - self.logger.debug('removed: {0}/'.format(d)) + self.logger.debug(f'removed: {d}/') warn_flag = True except OSError: pass diff --git a/nikola/plugins/command/deploy.py b/nikola/plugins/command/deploy.py index 9a47437d23..2ac3b442a3 100644 --- a/nikola/plugins/command/deploy.py +++ b/nikola/plugins/command/deploy.py @@ -69,7 +69,7 @@ def _execute(self, command, args): # Remove drafts and future posts if requested undeployed_posts = clean_before_deployment(self.site) if undeployed_posts: - self.logger.warning("Deleted {0} posts due to DEPLOY_* settings".format(len(undeployed_posts))) + self.logger.warning(f"Deleted {len(undeployed_posts)} posts due to DEPLOY_* settings") if args: presets = args @@ -81,18 +81,18 @@ def _execute(self, command, args): try: self.site.config['DEPLOY_COMMANDS'][preset] except KeyError: - self.logger.error('No such preset: {0}'.format(preset)) + self.logger.error(f'No such preset: {preset}') return 255 for preset in presets: - self.logger.info("=> preset '{0}'".format(preset)) + self.logger.info(f"=> preset '{preset}'") for command in self.site.config['DEPLOY_COMMANDS'][preset]: - self.logger.info("==> {0}".format(command)) + self.logger.info(f"==> {command}") try: subprocess.check_call(command, shell=True) except subprocess.CalledProcessError as e: - self.logger.error('Failed deployment -- command {0} ' - 'returned {1}'.format(e.cmd, e.returncode)) + self.logger.error(f'Failed deployment -- command {e.cmd} ' + f'returned {e.returncode}') return e.returncode self.logger.info("Successful deployment") diff --git a/nikola/plugins/command/github_deploy.py b/nikola/plugins/command/github_deploy.py index 50d216a8bf..aedec464c1 100644 --- a/nikola/plugins/command/github_deploy.py +++ b/nikola/plugins/command/github_deploy.py @@ -102,14 +102,14 @@ def _execute(self, options, args): # Remove drafts and future posts if requested (Issue #2406) undeployed_posts = clean_before_deployment(self.site) if undeployed_posts: - self.logger.warning("Deleted {0} posts due to DEPLOY_* settings".format(len(undeployed_posts))) + self.logger.warning(f"Deleted {len(undeployed_posts)} posts due to DEPLOY_* settings") # Commit and push return self._commit_and_push(options['commit_message']) def _run_command(self, command, xfail=False): """Run a command that may or may not fail.""" - self.logger.info("==> {0}".format(command)) + self.logger.info(f"==> {command}") try: subprocess.check_call(command) return 0 @@ -117,8 +117,8 @@ def _run_command(self, command, xfail=False): if xfail: return e.returncode self.logger.error( - 'Failed GitHub deployment -- command {0} ' - 'returned {1}'.format(e.cmd, e.returncode) + f'Failed GitHub deployment -- command {e.cmd} ' + f'returned {e.returncode}' ) raise DeployFailedException(e.returncode) @@ -131,8 +131,8 @@ def _commit_and_push(self, commit_first_line): try: if autocommit: commit_message = ( - '{0}\n\n' - 'Nikola version: {1}'.format(commit_first_line, __version__) + f'{commit_first_line}\n\n' + f'Nikola version: {__version__}' ) e = self._run_command(['git', 'checkout', source], True) if e != 0: @@ -154,9 +154,9 @@ def _commit_and_push(self, commit_first_line): source_commit = '?' commit_message = ( - '{0}\n\n' - 'Source commit: {1}' - 'Nikola version: {2}'.format(commit_first_line, source_commit, __version__) + f'{commit_first_line}\n\n' + f'Source commit: {source_commit}' + f'Nikola version: {__version__}' ) output_folder = self.site.config['OUTPUT_FOLDER'] diff --git a/nikola/plugins/command/import_wordpress.py b/nikola/plugins/command/import_wordpress.py index 9a8b130bd3..cdd54dda51 100644 --- a/nikola/plugins/command/import_wordpress.py +++ b/nikola/plugins/command/import_wordpress.py @@ -64,7 +64,7 @@ def install_plugin(site, plugin_name, output_dir=None, show_install_notes=False): """Install a Nikola plugin.""" - LOGGER.info("Installing plugin '{0}'".format(plugin_name)) + LOGGER.info(f"Installing plugin '{plugin_name}'") # Get hold of the 'plugin' plugin plugin_installer_info = site.plugin_manager.get_plugin_by_name('plugin', 'Command') if plugin_installer_info is None: @@ -273,9 +273,9 @@ def _read_options(self, options, args): options['output_folder'] = args.pop(0) if args: - LOGGER.warning('You specified additional arguments ({0}). Please consider ' + LOGGER.warning(f'You specified additional arguments ({args}). Please consider ' 'putting these arguments before the filename if you ' - 'are running into problems.'.format(args)) + 'are running into problems.') self.onefile = options.get('one_file', False) @@ -349,11 +349,11 @@ def _prepare(self, channel): if self.export_categories_as_categories: wordpress_namespace = channel.nsmap['wp'] cat_map = dict() - for cat in channel.findall('{{{0}}}category'.format(wordpress_namespace)): + for cat in channel.findall(f'{{{wordpress_namespace}}}category'): # cat_id = get_text_tag(cat, '{{{0}}}term_id'.format(wordpress_namespace), None) - cat_slug = get_text_tag(cat, '{{{0}}}category_nicename'.format(wordpress_namespace), None) - cat_parent_slug = get_text_tag(cat, '{{{0}}}category_parent'.format(wordpress_namespace), None) - cat_name = utils.html_unescape(get_text_tag(cat, '{{{0}}}cat_name'.format(wordpress_namespace), None)) + cat_slug = get_text_tag(cat, f'{{{wordpress_namespace}}}category_nicename', None) + cat_parent_slug = get_text_tag(cat, f'{{{wordpress_namespace}}}category_parent', None) + cat_name = utils.html_unescape(get_text_tag(cat, f'{{{wordpress_namespace}}}cat_name', None)) cat_path = [cat_name] if cat_parent_slug in cat_map: cat_path = cat_map[cat_parent_slug] + cat_path @@ -377,11 +377,9 @@ def _execute(self, options={}, args=[]): if not self.no_downloads: def show_info_about_mising_module(modulename): LOGGER.error( - 'To use the "{commandname}" command, you have to install ' - 'the "{package}" package or supply the "--no-downloads" ' - 'option.'.format( - commandname=self.name, - package=modulename) + f'To use the "{self.name}" command, you have to install ' + f'the "{modulename}" package or supply the "--no-downloads" ' + 'option.' ) if phpserialize is None: @@ -431,7 +429,7 @@ def show_info_about_mising_module(modulename): else: LOGGER.warning("Make sure to install the WordPress page compiler via") LOGGER.warning(" nikola plugin -i wordpress_compiler") - LOGGER.warning("in your imported blog's folder ({0}), if you haven't installed it system-wide or user-wide. Otherwise, your newly imported blog won't compile.".format(self.output_folder)) + LOGGER.warning(f"in your imported blog's folder ({self.output_folder}), if you haven't installed it system-wide or user-wide. Otherwise, your newly imported blog won't compile.") @classmethod def read_xml_file(cls, filename): @@ -477,7 +475,7 @@ def populate_context(self, channel): channel, 'description', 'PUT DESCRIPTION HERE') context['BASE_URL'] = get_text_tag(channel, 'link', '#') if not context['BASE_URL']: - base_site_url = channel.find('{{{0}}}author'.format(wordpress_namespace)) + base_site_url = channel.find(f'{{{wordpress_namespace}}}author') context['BASE_URL'] = get_text_tag(base_site_url, None, "http://foo.com/") @@ -485,14 +483,14 @@ def populate_context(self, channel): context['BASE_URL'] += '/' context['SITE_URL'] = context['BASE_URL'] - author = channel.find('{{{0}}}author'.format(wordpress_namespace)) + author = channel.find(f'{{{wordpress_namespace}}}author') context['BLOG_EMAIL'] = get_text_tag( author, - '{{{0}}}author_email'.format(wordpress_namespace), + f'{{{wordpress_namespace}}}author_email', "joe@example.com") context['BLOG_AUTHOR'] = get_text_tag( author, - '{{{0}}}author_display_name'.format(wordpress_namespace), + f'{{{wordpress_namespace}}}author_display_name', "Joe Example") extensions = ['rst', 'txt', 'md', 'html'] if self.use_wordpress_compiler: @@ -500,8 +498,8 @@ def populate_context(self, channel): POSTS = '(\n' PAGES = '(\n' for extension in extensions: - POSTS += ' ("posts/*.{0}", "posts", "post.tmpl"),\n'.format(extension) - PAGES += ' ("pages/*.{0}", "pages", "page.tmpl"),\n'.format(extension) + POSTS += f' ("posts/*.{extension}", "posts", "post.tmpl"),\n' + PAGES += f' ("pages/*.{extension}", "pages", "page.tmpl"),\n' POSTS += ')\n' PAGES += ')\n' context['POSTS'] = POSTS @@ -522,28 +520,28 @@ def download_url_content_to_file(self, url, dst_path): try: request = requests.get(url, auth=self.auth) if request.status_code >= 400: - LOGGER.warning("Downloading {0} to {1} failed with HTTP status code {2}".format(url, dst_path, request.status_code)) + LOGGER.warning(f"Downloading {url} to {dst_path} failed with HTTP status code {request.status_code}") return with open(dst_path, 'wb+') as fd: fd.write(request.content) except requests.exceptions.ConnectionError as err: - LOGGER.warning("Downloading {0} to {1} failed: {2}".format(url, dst_path, err)) + LOGGER.warning(f"Downloading {url} to {dst_path} failed: {err}") def import_attachment(self, item, wordpress_namespace): """Import an attachment to the site.""" # Download main image url = get_text_tag( - item, '{{{0}}}attachment_url'.format(wordpress_namespace), 'foo') - link = get_text_tag(item, '{{{0}}}link'.format(wordpress_namespace), + item, f'{{{wordpress_namespace}}}attachment_url', 'foo') + link = get_text_tag(item, f'{{{wordpress_namespace}}}link', 'foo') path = urlparse(url).path dst_path = os.path.join(*([self.output_folder, 'files'] + list(path.split('/')))) if self.no_downloads: - LOGGER.info("Skipping downloading {0} => {1}".format(url, dst_path)) + LOGGER.info(f"Skipping downloading {url} => {dst_path}") else: dst_dir = os.path.dirname(dst_path) utils.makedirs(dst_dir) - LOGGER.info("Downloading {0} => {1}".format(url, dst_path)) + LOGGER.info(f"Downloading {url} => {dst_path}") self.download_url_content_to_file(url, dst_path) dst_url = '/'.join(dst_path.split(os.sep)[2:]) links[link] = '/' + dst_url @@ -552,13 +550,13 @@ def import_attachment(self, item, wordpress_namespace): files = [path] files_meta = [{}] - additional_metadata = item.findall('{{{0}}}postmeta'.format(wordpress_namespace)) + additional_metadata = item.findall(f'{{{wordpress_namespace}}}postmeta') if phpserialize and additional_metadata: source_path = os.path.dirname(url) for element in additional_metadata: - meta_key = element.find('{{{0}}}meta_key'.format(wordpress_namespace)) + meta_key = element.find(f'{{{wordpress_namespace}}}meta_key') if meta_key is not None and meta_key.text == '_wp_attachment_metadata': - meta_value = element.find('{{{0}}}meta_value'.format(wordpress_namespace)) + meta_value = element.find(f'{{{wordpress_namespace}}}meta_value') if meta_value is None: continue @@ -644,11 +642,11 @@ def add(our_key, wp_key, is_int=False, ignore_zero=False, is_float=False): path = urlparse(url).path dst_path = os.path.join(*([self.output_folder, 'files'] + list(path.split('/')))) if self.no_downloads: - LOGGER.info("Skipping downloading {0} => {1}".format(url, dst_path)) + LOGGER.info(f"Skipping downloading {url} => {dst_path}") else: dst_dir = os.path.dirname(dst_path) utils.makedirs(dst_dir) - LOGGER.info("Downloading {0} => {1}".format(url, dst_path)) + LOGGER.info(f"Downloading {url} => {dst_path}") self.download_url_content_to_file(url, dst_path) dst_url = '/'.join(dst_path.split(os.sep)[2:]) links[url] = '/' + dst_url @@ -668,7 +666,7 @@ def add(our_key, wp_key, is_int=False, ignore_zero=False, is_float=False): def add(result_key, key, namespace=None, filter=None, store_empty=False): if namespace is not None: - value = get_text_tag(item, '{{{0}}}{1}'.format(namespace, key), None) + value = get_text_tag(item, f'{{{namespace}}}{key}', None) else: value = get_text_tag(item, key, None) if value is not None: @@ -708,7 +706,7 @@ def replacement(m, c=content): code = code.replace('>', '>') code = code.replace('<', '<') code = code.replace('"', '"') - return '```{language}\n{code}\n```'.format(language=language, code=code) + return f'```{language}\n{code}\n```' content = self.code_re1.sub(replacement, content) content = self.code_re2.sub(replacement, content) @@ -779,15 +777,15 @@ def transform_content(self, content, post_format, attachments): def _extract_comment(self, comment, wordpress_namespace): """Extract comment from dump.""" - id = int(get_text_tag(comment, "{{{0}}}comment_id".format(wordpress_namespace), None)) - author = get_text_tag(comment, "{{{0}}}comment_author".format(wordpress_namespace), None) - author_email = get_text_tag(comment, "{{{0}}}comment_author_email".format(wordpress_namespace), None) - author_url = get_text_tag(comment, "{{{0}}}comment_author_url".format(wordpress_namespace), None) - author_IP = get_text_tag(comment, "{{{0}}}comment_author_IP".format(wordpress_namespace), None) + id = int(get_text_tag(comment, f"{{{wordpress_namespace}}}comment_id", None)) + author = get_text_tag(comment, f"{{{wordpress_namespace}}}comment_author", None) + author_email = get_text_tag(comment, f"{{{wordpress_namespace}}}comment_author_email", None) + author_url = get_text_tag(comment, f"{{{wordpress_namespace}}}comment_author_url", None) + author_IP = get_text_tag(comment, f"{{{wordpress_namespace}}}comment_author_IP", None) # date = get_text_tag(comment, "{{{0}}}comment_date".format(wordpress_namespace), None) - date_gmt = get_text_tag(comment, "{{{0}}}comment_date_gmt".format(wordpress_namespace), None) - content = get_text_tag(comment, "{{{0}}}comment_content".format(wordpress_namespace), None) - approved = get_text_tag(comment, "{{{0}}}comment_approved".format(wordpress_namespace), '0') + date_gmt = get_text_tag(comment, f"{{{wordpress_namespace}}}comment_date_gmt", None) + content = get_text_tag(comment, f"{{{wordpress_namespace}}}comment_content", None) + approved = get_text_tag(comment, f"{{{wordpress_namespace}}}comment_approved", '0') if approved == '0': approved = 'hold' elif approved == '1': @@ -795,11 +793,11 @@ def _extract_comment(self, comment, wordpress_namespace): elif approved == 'spam' or approved == 'trash': pass else: - LOGGER.warning("Unknown comment approved status: {0}".format(approved)) - parent = int(get_text_tag(comment, "{{{0}}}comment_parent".format(wordpress_namespace), 0)) + LOGGER.warning(f"Unknown comment approved status: {approved}") + parent = int(get_text_tag(comment, f"{{{wordpress_namespace}}}comment_parent", 0)) if parent == 0: parent = None - user_id = int(get_text_tag(comment, "{{{0}}}comment_user_id".format(wordpress_namespace), 0)) + user_id = int(get_text_tag(comment, f"{{{wordpress_namespace}}}comment_user_id", 0)) if user_id == 0: user_id = None @@ -880,7 +878,7 @@ def _sanitize(self, tag, is_category): LOGGER.warning("Changing spelling of {0} name '{1}' to {2}.".format('category' if is_category else 'tag', tag, previous[0])) return previous[0] else: - LOGGER.error("Unknown tag sanitizing strategy '{0}'!".format(self.tag_saniziting_strategy)) + LOGGER.error(f"Unknown tag sanitizing strategy '{self.tag_saniziting_strategy}'!") sys.exit(1) return tag @@ -918,10 +916,10 @@ def import_postpage_item(self, item, wordpress_namespace, out_folder=None, attac if parsed.query: # if there are no nice URLs and query strings are used out_folder = os.path.join(*([out_folder] + pathlist)) slug = get_text_tag( - item, '{{{0}}}post_name'.format(wordpress_namespace), None) + item, f'{{{wordpress_namespace}}}post_name', None) if not slug: # it *may* happen slug = get_text_tag( - item, '{{{0}}}post_id'.format(wordpress_namespace), None) + item, f'{{{wordpress_namespace}}}post_id', None) if not slug: # should never happen LOGGER.error("Error converting post:", title) return False @@ -932,18 +930,18 @@ def import_postpage_item(self, item, wordpress_namespace, out_folder=None, attac description = get_text_tag(item, 'description', '') post_date = get_text_tag( - item, '{{{0}}}post_date'.format(wordpress_namespace), None) + item, f'{{{wordpress_namespace}}}post_date', None) try: dt = utils.to_datetime(post_date) except ValueError: dt = datetime.datetime(1970, 1, 1, 0, 0, 0) - LOGGER.error('Malformed date "{0}" in "{1}" [{2}], assuming 1970-01-01 00:00:00 instead.'.format(post_date, title, slug)) + LOGGER.error(f'Malformed date "{post_date}" in "{title}" [{slug}], assuming 1970-01-01 00:00:00 instead.') post_date = dt.strftime('%Y-%m-%d %H:%M:%S') if dt.tzinfo and self.timezone is None: self.timezone = utils.get_tzname(dt) status = get_text_tag( - item, '{{{0}}}status'.format(wordpress_namespace), 'publish') + item, f'{{{wordpress_namespace}}}status', 'publish') content = get_text_tag( item, '{http://purl.org/rss/1.0/modules/content/}encoded', '') excerpt = get_text_tag( @@ -958,7 +956,7 @@ def import_postpage_item(self, item, wordpress_namespace, out_folder=None, attac post_status = 'published' has_math = "no" if status == 'trash': - LOGGER.warning('Trashed post "{0}" will not be imported.'.format(title)) + LOGGER.warning(f'Trashed post "{title}" will not be imported.') return False elif status == 'private': is_draft = False @@ -1006,10 +1004,10 @@ def import_postpage_item(self, item, wordpress_namespace, out_folder=None, attac post_format = 'wp' if is_draft and self.exclude_drafts: - LOGGER.warning('Draft "{0}" will not be imported.'.format(title)) + LOGGER.warning(f'Draft "{title}" will not be imported.') return False elif is_private and self.exclude_privates: - LOGGER.warning('Private post "{0}" will not be imported.'.format(title)) + LOGGER.warning(f'Private post "{title}" will not be imported.') return False elif content.strip() or self.import_empty_items: # If no content is found, no files are written. @@ -1078,7 +1076,7 @@ def import_postpage_item(self, item, wordpress_namespace, out_folder=None, attac if self.export_comments: comments = [] - for tag in item.findall('{{{0}}}comment'.format(wordpress_namespace)): + for tag in item.findall(f'{{{wordpress_namespace}}}comment'): comment = self._extract_comment(tag, wordpress_namespace) if comment is not None: comments.append(comment) @@ -1089,8 +1087,8 @@ def import_postpage_item(self, item, wordpress_namespace, out_folder=None, attac return (out_folder, slug) else: - LOGGER.warning(('Not going to import "{0}" because it seems to contain' - ' no content.').format(title)) + LOGGER.warning((f'Not going to import "{title}" because it seems to contain' + ' no content.')) return False def _extract_item_info(self, item): @@ -1099,11 +1097,11 @@ def _extract_item_info(self, item): # http://wordpress.org/export/1.2/ wordpress_namespace = item.nsmap['wp'] post_type = get_text_tag( - item, '{{{0}}}post_type'.format(wordpress_namespace), 'post') + item, f'{{{wordpress_namespace}}}post_type', 'post') post_id = int(get_text_tag( - item, '{{{0}}}post_id'.format(wordpress_namespace), "0")) + item, f'{{{wordpress_namespace}}}post_id', "0")) parent_id = get_text_tag( - item, '{{{0}}}post_parent'.format(wordpress_namespace), None) + item, f'{{{wordpress_namespace}}}post_parent', None) return wordpress_namespace, post_type, post_id, parent_id def process_item_if_attachment(self, item): diff --git a/nikola/plugins/command/init.py b/nikola/plugins/command/init.py index b42af58256..5e68d61623 100644 --- a/nikola/plugins/command/init.py +++ b/nikola/plugins/command/init.py @@ -168,7 +168,7 @@ def format_default_translations_config(additional_languages): return SAMPLE_CONF["TRANSLATIONS"] lang_paths = [' DEFAULT_LANG: "",'] for lang in sorted(additional_languages): - lang_paths.append(' "{0}": "./{0}",'.format(lang)) + lang_paths.append(f' "{lang}": "./{lang}",') return "{{\n{0}\n}}".format("\n".join(lang_paths)) @@ -234,7 +234,7 @@ def test_destination(destination, demo=False): """Check if the destination already exists, which can break demo site creation.""" # Issue #2214 if demo and os.path.exists(destination): - LOGGER.warning("The directory {0} already exists, and a new demo site cannot be initialized in an existing directory.".format(destination)) + LOGGER.warning(f"The directory {destination} already exists, and a new demo site cannot be initialized in an existing directory.") LOGGER.warning("Please remove the directory and try again, or use another directory.") LOGGER.info("Hint: If you want to initialize a git repository in this directory, run `git init` in the directory after creating a Nikola site.") return False @@ -352,7 +352,7 @@ def lhandler(default, toconf, show_header=True): for partial, full in LEGAL_VALUES['_TRANSLATIONS_WITH_COUNTRY_SPECIFIERS'].items(): if partial in langs: langs[langs.index(partial)] = full - print("NOTICE: Assuming '{0}' instead of '{1}'.".format(full, partial)) + print(f"NOTICE: Assuming '{full}' instead of '{partial}'.") default = langs.pop(0) SAMPLE_CONF['DEFAULT_LANG'] = default @@ -370,7 +370,7 @@ def lhandler(default, toconf, show_header=True): messages = load_messages(['base'], tr, default, themes_dirs=['themes']) SAMPLE_CONF['NAVIGATION_LINKS'] = format_navigation_links(langs, default, messages, SAMPLE_CONF['STRIP_INDEXES']) except nikola.utils.LanguageNotFoundError as e: - print(" ERROR: the language '{0}' is not supported.".format(e.lang)) + print(f" ERROR: the language '{e.lang}' is not supported.") print(" Are you sure you spelled the name correctly? Names are case-sensitive and need to be reproduced as-is (complete with the country specifier, if any).") print("\nType '?' (a question mark, sans quotes) to list available languages.") lhandler(default, toconf, show_header=False) @@ -396,7 +396,7 @@ def tzhandler(default, toconf): if len(matching_zones) == 1: tz = dateutil.tz.gettz(matching_zones[0]) answer = matching_zones[0] - print(" Picking '{0}'.".format(answer)) + print(f" Picking '{answer}'.") elif len(matching_zones) > 1: print(" The following time zones match your query:") print(' ' + '\n '.join(matching_zones)) @@ -404,7 +404,7 @@ def tzhandler(default, toconf): if tz is not None: time = datetime.datetime.now(tz).strftime('%H:%M:%S') - print(" Current time in {0}: {1}".format(answer, time)) + print(f" Current time in {answer}: {time}") answered = ask_yesno("Use this time zone?", True) else: print(" ERROR: No matches found. Please try again.") @@ -457,7 +457,7 @@ def chandler(default, toconf): print("Creating Nikola Site") print("====================\n") - print("This is Nikola v{0}. We will now ask you a few easy questions about your new site.".format(nikola.__version__)) + print(f"This is Nikola v{nikola.__version__}. We will now ask you a few easy questions about your new site.") print("If you do not want to answer and want to go with the defaults instead, simply restart with the `-q` parameter.") for query, default, toconf, destination in questions: @@ -466,7 +466,7 @@ def chandler(default, toconf): pass else: if default is toconf is destination is None: - print('--- {0} ---'.format(query)) + print(f'--- {query} ---') elif destination is True: query(default, toconf) else: @@ -512,13 +512,13 @@ def _execute(self, options={}, args=None): return 1 if not options.get('demo'): self.create_empty_site(target) - LOGGER.info('Created empty site at {0}.'.format(target)) + LOGGER.info(f'Created empty site at {target}.') else: if not test_destination(target, True): return 2 self.copy_sample_site(target) LOGGER.info("A new site with example data has been created at " - "{0}.".format(target)) + f"{target}.") LOGGER.info("See README.txt in that folder for more information.") self.create_configuration(target) diff --git a/nikola/plugins/command/new_post.py b/nikola/plugins/command/new_post.py index 0734ec8b84..0a15399629 100644 --- a/nikola/plugins/command/new_post.py +++ b/nikola/plugins/command/new_post.py @@ -103,10 +103,10 @@ def get_date(schedule=False, rule=None, last_date=None, tz=None, iso8601=False): offset_hrs = offset_sec // 3600 offset_min = offset_sec % 3600 if iso8601: - tz_str = '{0:+03d}:{1:02d}'.format(offset_hrs, offset_min // 60) + tz_str = f'{offset_hrs:+03d}:{offset_min // 60:02d}' else: if offset: - tz_str = ' UTC{0:+03d}:{1:02d}'.format(offset_hrs, offset_min // 60) + tz_str = f' UTC{offset_hrs:+03d}:{offset_min // 60:02d}' else: tz_str = ' UTC' @@ -271,7 +271,7 @@ def _execute(self, options, args): if extension in extensions: content_format = compiler if not content_format: - LOGGER.error("Unknown {0} extension {1}, maybe you need to install a plugin or enable an existing one?".format(content_type, extension)) + LOGGER.error(f"Unknown {content_type} extension {extension}, maybe you need to install a plugin or enable an existing one?") return elif not content_format and import_file: @@ -282,7 +282,7 @@ def _execute(self, options, args): if extension in extensions: content_format = compiler if not content_format: - LOGGER.error("Unknown {0} extension {1}, maybe you need to install a plugin or enable an existing one?".format(content_type, extension)) + LOGGER.error(f"Unknown {content_type} extension {extension}, maybe you need to install a plugin or enable an existing one?") return elif not content_format: # Issue #400 @@ -292,7 +292,7 @@ def _execute(self, options, args): self.site.config['post_pages']) elif content_format not in compiler_names: - LOGGER.error("Unknown {0} format {1}, maybe you need to install a plugin or enable an existing one?".format(content_type, content_format)) + LOGGER.error(f"Unknown {content_type} format {content_format}, maybe you need to install a plugin or enable an existing one?") self.print_compilers() return @@ -306,10 +306,10 @@ def _execute(self, options, args): return 1 if import_file: - print("Importing Existing {xx}".format(xx=content_type.title())) + print(f"Importing Existing {content_type.title()}") print("-----------------------\n") else: - print("Creating New {xx}".format(xx=content_type.title())) + print(f"Creating New {content_type.title()}") print("-----------------\n") if title is not None: print("Title:", title) @@ -390,9 +390,9 @@ def _execute(self, options, args): signal('existing_' + content_type).send(self, **event) LOGGER.error("The title already exists!") - LOGGER.info("Existing {0}'s text is at: {1}".format(content_type, txt_path)) + LOGGER.info(f"Existing {content_type}'s text is at: {txt_path}") if not onefile: - LOGGER.info("Existing {0}'s metadata is at: {1}".format(content_type, meta_path)) + LOGGER.info(f"Existing {content_type}'s metadata is at: {meta_path}") return 8 d_name = os.path.dirname(txt_path) @@ -435,9 +435,9 @@ def _execute(self, options, args): if not onefile: # write metadata file with io.open(meta_path, "w+", encoding="utf8") as fd: fd.write(utils.write_metadata(data, comment_wrap=False, site=self.site)) - LOGGER.info("Your {0}'s metadata is at: {1}".format(content_type, meta_path)) + LOGGER.info(f"Your {content_type}'s metadata is at: {meta_path}") event['meta_path'] = meta_path - LOGGER.info("Your {0}'s text is at: {1}".format(content_type, txt_path)) + LOGGER.info(f"Your {content_type}'s text is at: {txt_path}") signal('new_' + content_type).send(self, **event) @@ -470,10 +470,10 @@ def filter_post_pages(self, compiler, is_post): extensions = compilers.get(compiler) if extensions is None: if compiler in compiler_objs: - LOGGER.error("There is a {0} compiler available, but it's not set in your COMPILERS option.".format(compiler)) - LOGGER.info("Read more: {0}".format(COMPILERS_DOC_LINK)) + LOGGER.error(f"There is a {compiler} compiler available, but it's not set in your COMPILERS option.") + LOGGER.info(f"Read more: {COMPILERS_DOC_LINK}") else: - LOGGER.error('Unknown format {0}'.format(compiler)) + LOGGER.error(f'Unknown format {compiler}') self.print_compilers() return False @@ -484,10 +484,9 @@ def filter_post_pages(self, compiler, is_post): if not filtered: type_name = "post" if is_post else "page" LOGGER.error("Can't find a way, using your configuration, to create " - "a {0} in format {1}. You may want to tweak " - "COMPILERS or {2}S in conf.py".format( - type_name, compiler, type_name.upper())) - LOGGER.info("Read more: {0}".format(COMPILERS_DOC_LINK)) + f"a {type_name} in format {compiler}. You may want to tweak " + f"COMPILERS or {type_name.upper()}S in conf.py") + LOGGER.info(f"Read more: {COMPILERS_DOC_LINK}") return False return filtered[0] @@ -558,10 +557,10 @@ def print_compilers(self): print(('{flag}{name:<' + str(name_width) + '} {fname:<' + str(fname_width) + '} {extensions}').format(flag=flag, name=name, fname=fname, extensions=extensions)) - print(""" + print(f""" More compilers are available in the Plugins Index. Compilers marked with ! and ~ require additional configuration: ! not in the POSTS/PAGES tuples and any post scanners (unused) ~ not in the COMPILERS dict (disabled) - Read more: {0}""".format(COMPILERS_DOC_LINK)) + Read more: {COMPILERS_DOC_LINK}""") diff --git a/nikola/plugins/command/plugin.py b/nikola/plugins/command/plugin.py index f562968a09..0967be99d7 100644 --- a/nikola/plugins/command/plugin.py +++ b/nikola/plugins/command/plugin.py @@ -195,7 +195,7 @@ def do_upgrade(self, url): LOGGER.info('Will upgrade {0} plugins: {1}'.format(len(plugins), ', '.join(n for n, _ in plugins))) for name, path in plugins: path: pathlib.Path - LOGGER.info('Upgrading {0}'.format(name)) + LOGGER.info(f'Upgrading {name}') p = path while True: tail, head = path.parent, path.name @@ -203,7 +203,7 @@ def do_upgrade(self, url): self.output_dir = path break elif path == tail: - LOGGER.error("Can't find the plugins folder for path: {0}".format(p)) + LOGGER.error(f"Can't find the plugins folder for path: {p}") return 1 else: path = tail @@ -216,12 +216,12 @@ def do_install(self, url, name, show_install_notes=True): if name in data: utils.makedirs(self.output_dir) url = data[name] - LOGGER.info("Downloading '{0}'".format(url)) + LOGGER.info(f"Downloading '{url}'") zip_data = requests.get(url).content zip_file = io.BytesIO() zip_file.write(zip_data) - LOGGER.info('Extracting: {0} into {1}/'.format(name, self.output_dir)) + LOGGER.info(f'Extracting: {name} into {self.output_dir}/') utils.extract_all(zip_file, self.output_dir) dest_path = self.output_dir / name else: @@ -288,15 +288,15 @@ def do_uninstall(self, name): for found_name, path in self.get_plugins(): if name == found_name: # Uninstall this one to_delete = path.parent # Delete parent of .py file or parent of package - LOGGER.warning('About to uninstall plugin: {0}'.format(name)) - LOGGER.warning('This will delete {0}'.format(to_delete)) + LOGGER.warning(f'About to uninstall plugin: {name}') + LOGGER.warning(f'This will delete {to_delete}') sure = utils.ask_yesno('Are you sure?') if sure: - LOGGER.warning('Removing {0}'.format(to_delete)) + LOGGER.warning(f'Removing {to_delete}') shutil.rmtree(to_delete) return 0 return 1 - LOGGER.error('Unknown plugin: {0}'.format(name)) + LOGGER.error(f'Unknown plugin: {name}') return 1 def get_json(self, url): diff --git a/nikola/plugins/command/serve.py b/nikola/plugins/command/serve.py index 0b2cbac564..7ad5f846c2 100644 --- a/nikola/plugins/command/serve.py +++ b/nikola/plugins/command/serve.py @@ -124,7 +124,7 @@ def _execute(self, options, args): """Start test server.""" out_dir = self.site.config['OUTPUT_FOLDER'] if not os.path.isdir(out_dir): - self.logger.error("Missing '{0}' folder?".format(out_dir)) + self.logger.error(f"Missing '{out_dir}' folder?") else: self.serve_pidfile = os.path.abspath('nikolaserve.pid') os.chdir(out_dir) @@ -151,13 +151,13 @@ def _execute(self, options, args): server_url = "http://[{0}]:{1}/".format(*sa) + base_path else: server_url = "http://{0}:{1}/".format(*sa) + base_path - self.logger.info("Serving on {0} ...".format(server_url)) + self.logger.info(f"Serving on {server_url} ...") if options['browser']: # Some browsers fail to load 0.0.0.0 (Issue #2755) if sa[0] == '0.0.0.0': server_url = "http://127.0.0.1:{1}/".format(*sa) + base_path - self.logger.info("Opening {0} in the default web browser...".format(server_url)) + self.logger.info(f"Opening {server_url} in the default web browser...") webbrowser.open(server_url) if options['detach']: OurHTTPRequestHandler.quiet = True @@ -168,8 +168,8 @@ def _execute(self, options, args): self.httpd.serve_forever() else: with open(self.serve_pidfile, 'w') as fh: - fh.write('{0}\n'.format(pid)) - self.logger.info("Detached with PID {0}. Run `kill {0}` or `kill $(cat nikolaserve.pid)` to stop the server.".format(pid)) + fh.write(f'{pid}\n') + self.logger.info(f"Detached with PID {pid}. Run `kill {pid}` or `kill $(cat nikolaserve.pid)` to stop the server.") except AttributeError: if os.name == 'nt': self.logger.warning("Detaching is not available on Windows, server is running in the foreground.") @@ -270,7 +270,7 @@ def send_head(self): # transmitted *less* than the content-length! f = open(path, 'rb') except IOError: - self.send_error(404, "File not found: {}".format(path)) + self.send_error(404, f"File not found: {path}") return None filtered_bytes = None @@ -287,7 +287,7 @@ def send_head(self): self.send_response(200) if ctype.startswith('text/') or ctype.endswith('+xml'): - self.send_header("Content-Type", "{0}; charset=UTF-8".format(ctype)) + self.send_header("Content-Type", f"{ctype}; charset=UTF-8") else: self.send_header("Content-Type", ctype) if os.path.splitext(path)[1] == '.svgz': diff --git a/nikola/plugins/command/status.py b/nikola/plugins/command/status.py index 878a98142f..9418369efb 100644 --- a/nikola/plugins/command/status.py +++ b/nikola/plugins/command/status.py @@ -109,12 +109,12 @@ def _execute(self, options, args): fmod_since_deployment.append(fpath) if len(fmod_since_deployment) > 0: - print("{0} output files modified since last deployment {1} ago.".format(str(len(fmod_since_deployment)), self.human_time(last_deploy_offset))) + print(f"{str(len(fmod_since_deployment))} output files modified since last deployment {self.human_time(last_deploy_offset)} ago.") if options['list_modified']: for fpath in fmod_since_deployment: - print("Modified: '{0}'".format(fpath)) + print(f"Modified: '{fpath}'") else: - print("Last deployment {0} ago.".format(self.human_time(last_deploy_offset))) + print(f"Last deployment {self.human_time(last_deploy_offset)} ago.") now = datetime.utcnow().replace(tzinfo=gettz("UTC")) @@ -155,7 +155,7 @@ def _execute(self, options, args): if options['list_published']: for post in posts_published: print("Published: '{0}' ({1}; source: {2})".format(post.meta('title'), post.permalink(), post.source_path)) - print("{0} posts in total, {1} scheduled, {2} drafts, {3} private and {4} published.".format(posts_count, len(posts_scheduled), len(posts_drafts), len(posts_private), len(posts_published))) + print(f"{posts_count} posts in total, {len(posts_scheduled)} scheduled, {len(posts_drafts)} drafts, {len(posts_private)} private and {len(posts_published)} published.") def human_time(self, dt): """Translate time into a human-friendly representation.""" @@ -163,9 +163,9 @@ def human_time(self, dt): hours = dt.seconds / 60 // 60 minutes = dt.seconds / 60 - (hours * 60) if days > 0: - return "{0:.0f} days and {1:.0f} hours".format(days, hours) + return f"{days:.0f} days and {hours:.0f} hours" elif hours > 0: - return "{0:.0f} hours and {1:.0f} minutes".format(hours, minutes) + return f"{hours:.0f} hours and {minutes:.0f} minutes" elif minutes: - return "{0:.0f} minutes".format(minutes) + return f"{minutes:.0f} minutes" return False diff --git a/nikola/plugins/command/subtheme.py b/nikola/plugins/command/subtheme.py index a289baf402..652ee7c710 100644 --- a/nikola/plugins/command/subtheme.py +++ b/nikola/plugins/command/subtheme.py @@ -101,8 +101,7 @@ def _execute(self, options, args): LOGGER.warning( '"subtheme" doesn\'t work well with the bootstrap3-gradients family') - LOGGER.info("Creating '{0}' theme from '{1}' and '{2}'".format( - name, swatch, parent)) + LOGGER.info(f"Creating '{name}' theme from '{swatch}' and '{parent}'") utils.makedirs(os.path.join('themes', name, 'assets', 'css')) for fname in ('bootstrap.min.css', 'bootstrap.css'): if swatch in [ @@ -116,11 +115,9 @@ def _execute(self, options, args): 'The hackertheme subthemes are only available for Bootstrap 4.') return 1 if fname == 'bootstrap.css': - url = 'https://raw.githubusercontent.com/HackerThemes/theme-machine/master/dist/{swatch}/css/bootstrap4-{swatch}.css'.format( - swatch=swatch) + url = f'https://raw.githubusercontent.com/HackerThemes/theme-machine/master/dist/{swatch}/css/bootstrap4-{swatch}.css' else: - url = 'https://raw.githubusercontent.com/HackerThemes/theme-machine/master/dist/{swatch}/css/bootstrap4-{swatch}.min.css'.format( - swatch=swatch) + url = f'https://raw.githubusercontent.com/HackerThemes/theme-machine/master/dist/{swatch}/css/bootstrap4-{swatch}.min.css' else: # Bootswatch url = 'https://bootswatch.com' if version: @@ -147,4 +144,4 @@ def _execute(self, options, args): cp.write(output) LOGGER.info( - 'Theme created. Change the THEME setting to "{0}" to use it.'.format(name)) + f'Theme created. Change the THEME setting to "{name}" to use it.') diff --git a/nikola/plugins/command/theme.py b/nikola/plugins/command/theme.py index 23eec6882f..65c6042f09 100644 --- a/nikola/plugins/command/theme.py +++ b/nikola/plugins/command/theme.py @@ -200,14 +200,14 @@ def do_install_deps(self, url, name): self.do_install(parent_name, data) name = parent_name if installstatus: - LOGGER.info('Remember to set THEME="{0}" in conf.py to use this theme.'.format(origname)) + LOGGER.info(f'Remember to set THEME="{origname}" in conf.py to use this theme.') def do_install(self, name, data): """Download and install a theme.""" if name in data: utils.makedirs(self.output_dir) url = data[name] - LOGGER.info("Downloading '{0}'".format(url)) + LOGGER.info(f"Downloading '{url}'") try: zip_data = requests.get(url).content except requests.exceptions.SSLError: @@ -218,16 +218,16 @@ def do_install(self, name, data): zip_file = io.BytesIO() zip_file.write(zip_data) - LOGGER.info("Extracting '{0}' into themes/".format(name)) + LOGGER.info(f"Extracting '{name}' into themes/") utils.extract_all(zip_file) dest_path = os.path.join(self.output_dir, name) else: dest_path = os.path.join(self.output_dir, name) try: theme_path = utils.get_theme_path_real(name, self.site.themes_dirs) - LOGGER.error("Theme '{0}' is already installed in {1}".format(name, theme_path)) + LOGGER.error(f"Theme '{name}' is already installed in {theme_path}") except Exception: - LOGGER.error("Can't find theme {0}".format(name)) + LOGGER.error(f"Can't find theme {name}") return False @@ -247,18 +247,18 @@ def do_uninstall(self, name): try: path = utils.get_theme_path_real(name, self.site.themes_dirs) except Exception: - LOGGER.error('Unknown theme: {0}'.format(name)) + LOGGER.error(f'Unknown theme: {name}') return 1 # Don't uninstall builtin themes (Issue #2510) blocked = os.path.dirname(utils.__file__) if path.startswith(blocked): - LOGGER.error("Can't delete builtin theme: {0}".format(name)) + LOGGER.error(f"Can't delete builtin theme: {name}") return 1 - LOGGER.warning('About to uninstall theme: {0}'.format(name)) - LOGGER.warning('This will delete {0}'.format(path)) + LOGGER.warning(f'About to uninstall theme: {name}') + LOGGER.warning(f'This will delete {path}') sure = utils.ask_yesno('Are you sure?') if sure: - LOGGER.warning('Removing {0}'.format(path)) + LOGGER.warning(f'Removing {path}') shutil.rmtree(path) return 0 return 1 @@ -293,14 +293,14 @@ def list_installed(self): for tname, tpath in sorted(set(themes)): if os.path.isdir(tpath): - print("{0} at {1}".format(tname, tpath)) + print(f"{tname} at {tpath}") def copy_template(self, template): """Copy the named template file from the parent to a local theme or to templates/.""" # Find template t = self.site.template_system.get_template_path(template) if t is None: - LOGGER.error("Cannot find template {0} in the lookup.".format(template)) + LOGGER.error(f"Cannot find template {template} in the lookup.") return 2 # Figure out where to put it. @@ -315,35 +315,35 @@ def copy_template(self, template): if not os.path.exists(base): os.mkdir(base) - LOGGER.info("Created directory {0}".format(base)) + LOGGER.info(f"Created directory {base}") try: out = shutil.copy(t, base) - LOGGER.info("Copied template from {0} to {1}".format(t, out)) + LOGGER.info(f"Copied template from {t} to {out}") except shutil.SameFileError: - LOGGER.error("This file already exists in your templates directory ({0}).".format(base)) + LOGGER.error(f"This file already exists in your templates directory ({base}).") return 3 def new_theme(self, name, engine, parent, create_legacy_meta=False): """Create a new theme.""" base = 'themes' themedir = os.path.join(base, name) - LOGGER.info("Creating theme {0} with parent {1} and engine {2} in {3}".format(name, parent, engine, themedir)) + LOGGER.info(f"Creating theme {name} with parent {parent} and engine {engine} in {themedir}") if not os.path.exists(base): os.mkdir(base) - LOGGER.info("Created directory {0}".format(base)) + LOGGER.info(f"Created directory {base}") # Check if engine and parent match parent_engine = utils.get_template_engine(utils.get_theme_chain(parent, self.site.themes_dirs)) if parent_engine != engine: - LOGGER.error("Cannot use engine {0} because parent theme '{1}' uses {2}".format(engine, parent, parent_engine)) + LOGGER.error(f"Cannot use engine {engine} because parent theme '{parent}' uses {parent_engine}") return 2 # Create theme if not os.path.exists(themedir): os.mkdir(themedir) - LOGGER.info("Created directory {0}".format(themedir)) + LOGGER.info(f"Created directory {themedir}") else: LOGGER.error("Theme already exists") return 2 @@ -357,7 +357,7 @@ def new_theme(self, name, engine, parent, create_legacy_meta=False): theme_meta_path = os.path.join(themedir, name + '.theme') with io.open(theme_meta_path, 'w', encoding='utf-8') as fh: cp.write(fh) - LOGGER.info("Created file {0}".format(theme_meta_path)) + LOGGER.info(f"Created file {theme_meta_path}") if create_legacy_meta: with io.open(os.path.join(themedir, 'parent'), 'w', encoding='utf-8') as fh: @@ -367,8 +367,8 @@ def new_theme(self, name, engine, parent, create_legacy_meta=False): fh.write(engine + '\n') LOGGER.info("Created file {0}".format(os.path.join(themedir, 'engine'))) - LOGGER.info("Theme {0} created successfully.".format(themedir)) - LOGGER.info('Remember to set THEME="{0}" in conf.py to use this theme.'.format(name)) + LOGGER.info(f"Theme {themedir} created successfully.") + LOGGER.info(f'Remember to set THEME="{name}" in conf.py to use this theme.') def get_json(self, url): """Download the JSON file with all plugins.""" diff --git a/nikola/plugins/command/version.py b/nikola/plugins/command/version.py index ca2184b9bc..7419be86ab 100644 --- a/nikola/plugins/command/version.py +++ b/nikola/plugins/command/version.py @@ -63,6 +63,6 @@ def _execute(self, options={}, args=None): if pypi_version == __version__: print("Nikola is up-to-date") else: - print("The latest version of Nikola is v{0}. Please upgrade " - "using `pip install --upgrade Nikola=={0}` or your " - "system package manager.".format(pypi_version)) + print(f"The latest version of Nikola is v{pypi_version}. Please upgrade " + f"using `pip install --upgrade Nikola=={pypi_version}` or your " + "system package manager.") diff --git a/nikola/plugins/compile/ipynb.py b/nikola/plugins/compile/ipynb.py index 32a19624fb..47b8e102a9 100644 --- a/nikola/plugins/compile/ipynb.py +++ b/nikola/plugins/compile/ipynb.py @@ -142,7 +142,7 @@ def create_post(self, path, **kw): if kernel is None: kernel = self.default_kernel - self.logger.warning('No kernel specified, assuming "{0}".'.format(kernel)) + self.logger.warning(f'No kernel specified, assuming "{kernel}".') IPYNB_KERNELS = {} ksm = kernelspec.KernelSpecManager() @@ -152,9 +152,9 @@ def create_post(self, path, **kw): del IPYNB_KERNELS[k]['argv'] if kernel not in IPYNB_KERNELS: - self.logger.error('Unknown kernel "{0}". Maybe you mispelled it?'.format(kernel)) + self.logger.error(f'Unknown kernel "{kernel}". Maybe you mispelled it?') self.logger.info("Available kernels: {0}".format(", ".join(sorted(IPYNB_KERNELS)))) - raise Exception('Unknown kernel "{0}"'.format(kernel)) + raise Exception(f'Unknown kernel "{kernel}"') nb["metadata"]["kernelspec"] = IPYNB_KERNELS[kernel] diff --git a/nikola/plugins/compile/markdown/mdx_gist.py b/nikola/plugins/compile/markdown/mdx_gist.py index fc24d34347..eaaecd9024 100644 --- a/nikola/plugins/compile/markdown/mdx_gist.py +++ b/nikola/plugins/compile/markdown/mdx_gist.py @@ -108,8 +108,7 @@ class GistFetchException(Exception): def __init__(self, url, status_code): """Initialize the exception.""" Exception.__init__(self) - self.message = 'Received a {0} response from Gist URL: {1}'.format( - status_code, url) + self.message = f'Received a {status_code} response from Gist URL: {url}' class GistPattern(InlineProcessor): @@ -168,7 +167,7 @@ def handleMatch(self, m, _): except GistFetchException as e: LOGGER.warning(e.message) - warning_comment = etree.Comment(' WARNING: {0} '.format(e.message)) + warning_comment = etree.Comment(f' WARNING: {e.message} ') noscript_elem.append(warning_comment) return (gist_elem, m.start(0), m.end(0)) diff --git a/nikola/plugins/compile/pandoc.py b/nikola/plugins/compile/pandoc.py index 8e6c6a8101..57e04108ce 100644 --- a/nikola/plugins/compile/pandoc.py +++ b/nikola/plugins/compile/pandoc.py @@ -62,10 +62,10 @@ def _get_pandoc_options(self, source: str) -> List[str]: try: pandoc_options = list(config_options[ext]) except KeyError: - self.logger.warning('Setting PANDOC_OPTIONS to [], because extension {} is not defined in PANDOC_OPTIONS: {}.'.format(ext, config_options)) + self.logger.warning(f'Setting PANDOC_OPTIONS to [], because extension {ext} is not defined in PANDOC_OPTIONS: {config_options}.') pandoc_options = [] else: - self.logger.warning('Setting PANDOC_OPTIONS to [], because PANDOC_OPTIONS is expected to be of type Union[List[str], Dict[str, List[str]]] but this is not: {}'.format(config_options)) + self.logger.warning(f'Setting PANDOC_OPTIONS to [], because PANDOC_OPTIONS is expected to be of type Union[List[str], Dict[str, List[str]]] but this is not: {config_options}') pandoc_options = [] return pandoc_options diff --git a/nikola/plugins/compile/php.py b/nikola/plugins/compile/php.py index 3438bcb72a..363291e4b0 100644 --- a/nikola/plugins/compile/php.py +++ b/nikola/plugins/compile/php.py @@ -46,7 +46,7 @@ def compile(self, source, dest, is_two_file=True, post=None, lang=None): with io.open(dest, "w+", encoding="utf8") as out_file: with open(source, "rb") as in_file: hash = md5(in_file.read()).hexdigest() - out_file.write(''.format(source, hash)) + out_file.write(f'') return True def compile_string(self, data, source_path=None, is_two_file=True, post=None, lang=None): diff --git a/nikola/plugins/compile/rest/doc.py b/nikola/plugins/compile/rest/doc.py index f0afeec6fc..d2db15bb51 100644 --- a/nikola/plugins/compile/rest/doc.py +++ b/nikola/plugins/compile/rest/doc.py @@ -98,14 +98,14 @@ def doc_role(name, rawtext, text, lineno, inliner, options={}, content=[]): if success: if twin_slugs: inliner.reporter.warning( - 'More than one post with the same slug. Using "{0}"'.format(permalink)) + f'More than one post with the same slug. Using "{permalink}"') LOGGER.warning( - 'More than one post with the same slug. Using "{0}" for doc role'.format(permalink)) + f'More than one post with the same slug. Using "{permalink}" for doc role') node = make_link_node(rawtext, title, permalink, options) return [node], [] else: msg = inliner.reporter.error( - '"{0}" slug doesn\'t exist.'.format(slug), + f'"{slug}" slug doesn\'t exist.', line=lineno) prb = inliner.problematic(rawtext, rawtext, msg) return [prb], [msg] @@ -118,12 +118,12 @@ def doc_shortcode(*args, **kwargs): if success: if twin_slugs: LOGGER.warning( - 'More than one post with the same slug. Using "{0}" for doc shortcode'.format(permalink)) - return '{1}'.format(permalink, title) + f'More than one post with the same slug. Using "{permalink}" for doc shortcode') + return f'{title}' else: LOGGER.error( - '"{0}" slug doesn\'t exist.'.format(slug)) - return 'Invalid link: {0}'.format(text) + f'"{slug}" slug doesn\'t exist.') + return f'Invalid link: {text}' def make_link_node(rawtext, text, url, options): diff --git a/nikola/plugins/compile/rest/gist.py b/nikola/plugins/compile/rest/gist.py index 08aa46d22f..86f82df3e5 100644 --- a/nikola/plugins/compile/rest/gist.py +++ b/nikola/plugins/compile/rest/gist.py @@ -47,11 +47,11 @@ def get_raw_gist_with_filename(self, gistID, filename): def get_raw_gist(self, gistID): """Get raw gist text.""" - url = "https://gist.github.com/raw/{0}".format(gistID) + url = f"https://gist.github.com/raw/{gistID}" try: return requests.get(url).text except requests.exceptions.RequestException: - raise self.error('Cannot get gist for url={0}'.format(url)) + raise self.error(f'Cannot get gist for url={url}') def run(self): """Run the gist directive.""" @@ -65,12 +65,12 @@ def run(self): if 'file' in self.options: filename = self.options['file'] rawGist = (self.get_raw_gist_with_filename(gistID, filename)) - embedHTML = ('').format(gistID, filename) + embedHTML = (f'') else: rawGist = (self.get_raw_gist(gistID)) - embedHTML = ('').format(gistID) + embedHTML = (f'') reqnode = nodes.literal_block('', rawGist) diff --git a/nikola/plugins/compile/rest/listing.py b/nikola/plugins/compile/rest/listing.py index f358f281f7..7b0fac77c9 100644 --- a/nikola/plugins/compile/rest/listing.py +++ b/nikola/plugins/compile/rest/listing.py @@ -86,7 +86,7 @@ def run(self): try: lexer = get_lexer_by_name(language) except pygments.util.ClassNotFound: - raise self.error('Cannot find pygments lexer for language "{0}"'.format(language)) + raise self.error(f'Cannot find pygments lexer for language "{language}"') if 'number-lines' in self.options: linenos = 'table' @@ -216,8 +216,7 @@ def run(self): src_target = urlunsplit(("link", 'listing_source', fpath.replace('\\', '/'), '', '')) src_label = self.site.MESSAGES('Source') generated_nodes = ( - [core.publish_doctree('`{0} <{1}>`_ `({2}) <{3}>`_' .format( - _fname, target, src_label, src_target))[0]]) + [core.publish_doctree(f'`{_fname} <{target}>`_ `({src_label}) <{src_target}>`_' )[0]]) generated_nodes += self.get_code_from_file(fileobject) return generated_nodes diff --git a/nikola/plugins/compile/rest/media.py b/nikola/plugins/compile/rest/media.py index 80526732e4..91f8cbfb10 100644 --- a/nikola/plugins/compile/rest/media.py +++ b/nikola/plugins/compile/rest/media.py @@ -67,6 +67,6 @@ def run(self): def _gen_media_embed(url, *q, **kw): if micawber is None: msg = req_missing(['micawber'], 'use the media directive', optional=True) - return '
{0}
'.format(msg) + return f'
{msg}
' providers = micawber.bootstrap_basic() return micawber.parse_text(url, providers) diff --git a/nikola/plugins/compile/rest/vimeo.py b/nikola/plugins/compile/rest/vimeo.py index 544ea4be8b..eb9d2fa284 100644 --- a/nikola/plugins/compile/rest/vimeo.py +++ b/nikola/plugins/compile/rest/vimeo.py @@ -114,8 +114,8 @@ def set_video_size(self): if json: # we can attempt to retrieve video attributes from vimeo try: - url = ('https://vimeo.com/api/v2/video/{0}' - '.json'.format(self.arguments[0])) + url = (f'https://vimeo.com/api/v2/video/{self.arguments[0]}' + '.json') data = requests.get(url).text video_attributes = json.loads(data)[0] self.options['height'] = video_attributes['height'] diff --git a/nikola/plugins/misc/scan_posts.py b/nikola/plugins/misc/scan_posts.py index 67757ea35a..0a94c44f02 100644 --- a/nikola/plugins/misc/scan_posts.py +++ b/nikola/plugins/misc/scan_posts.py @@ -102,7 +102,7 @@ def scan(self): seen.add(post.translated_source_path(lang)) timeline.append(post) except Exception: - LOGGER.error('Error reading post {}'.format(base_path)) + LOGGER.error(f'Error reading post {base_path}') raise return timeline diff --git a/nikola/plugins/misc/taxonomies_classifier.py b/nikola/plugins/misc/taxonomies_classifier.py index f1591c7bfa..16440e33ea 100644 --- a/nikola/plugins/misc/taxonomies_classifier.py +++ b/nikola/plugins/misc/taxonomies_classifier.py @@ -69,7 +69,7 @@ def _do_classification(self, site): # Extract classifications for this language classifications[lang] = taxonomy.classify(post, lang) if not taxonomy.more_than_one_classifications_per_post and len(classifications[lang]) > 1: - raise ValueError("Too many {0} classifications for post {1}".format(taxonomy.classification_name, post.source_path)) + raise ValueError(f"Too many {taxonomy.classification_name} classifications for post {post.source_path}") # Add post to sets for classification in classifications[lang]: while True: @@ -181,8 +181,7 @@ def create_hierarchy(hierarchy, parent=None, level=0): if other_classification_name == taxonomy.classification_name and other_classification == classification: taxonomy_outputs[lang][path][2].extend(filtered_posts) else: - utils.LOGGER.error('You have classifications that are too similar: {0} "{1}" and {2} "{3}" both result in output path {4} for language {5}.'.format( - taxonomy.classification_name, classification, other_classification_name, other_classification, path, lang)) + utils.LOGGER.error(f'You have classifications that are too similar: {taxonomy.classification_name} "{classification}" and {other_classification_name} "{other_classification}" both result in output path {path} for language {lang}.') utils.LOGGER.error('{0} "{1}" is used in: {2}'.format( taxonomy.classification_name.title(), classification, ', '.join(sorted([p.source_path for p in filtered_posts])))) utils.LOGGER.error('{0} "{1}" is used in: {2}'.format( diff --git a/nikola/plugins/shortcode/chart.py b/nikola/plugins/shortcode/chart.py index 71fbcc293b..625347212b 100644 --- a/nikola/plugins/shortcode/chart.py +++ b/nikola/plugins/shortcode/chart.py @@ -48,7 +48,7 @@ def handler(self, chart_type, **_options): if pygal is None: msg = req_missing( ['pygal'], 'use the Chart directive', optional=True) - return '
{0}
'.format(msg) + return f'
{msg}
' options = {} chart_data = [] _options.pop('post', None) @@ -58,7 +58,7 @@ def handler(self, chart_type, **_options): for line in data.splitlines(): line = line.strip() if line: - chart_data.append(literal_eval('({0})'.format(line))) + chart_data.append(literal_eval(f'({line})')) if 'data_file' in _options: options = load_data(_options['data_file']) _options.pop('data_file') diff --git a/nikola/plugins/shortcode/emoji/__init__.py b/nikola/plugins/shortcode/emoji/__init__.py index 9ae2228043..030cb528a9 100644 --- a/nikola/plugins/shortcode/emoji/__init__.py +++ b/nikola/plugins/shortcode/emoji/__init__.py @@ -38,9 +38,9 @@ def handler(self, name, filename=None, site=None, data=None, lang=None, post=Non if not TABLE: _populate() try: - output = u'''{}'''.format(TABLE[name]) + output = f'''{TABLE[name]}''' except KeyError: - LOGGER.warning('Unknown emoji {}'.format(name)) - output = u'''{}'''.format(name) + LOGGER.warning(f'Unknown emoji {name}') + output = f'''{name}''' return output, [] diff --git a/nikola/plugins/shortcode/gist.py b/nikola/plugins/shortcode/gist.py index eb9e97677b..7a14673233 100644 --- a/nikola/plugins/shortcode/gist.py +++ b/nikola/plugins/shortcode/gist.py @@ -20,11 +20,11 @@ def get_raw_gist_with_filename(self, gistID, filename): def get_raw_gist(self, gistID): """Get raw gist text.""" - url = "https://gist.github.com/raw/{0}".format(gistID) + url = f"https://gist.github.com/raw/{gistID}" try: return requests.get(url).text except requests.exceptions.RequestException: - raise self.error('Cannot get gist for url={0}'.format(url)) + raise self.error(f'Cannot get gist for url={url}') def handler(self, gistID, filename=None, site=None, data=None, lang=None, post=None): """Create HTML for gist.""" @@ -37,14 +37,14 @@ def handler(self, gistID, filename=None, site=None, data=None, lang=None, post=N if filename is not None: rawGist = (self.get_raw_gist_with_filename(gistID, filename)) - embedHTML = ('').format(gistID, filename) + embedHTML = (f'') else: rawGist = (self.get_raw_gist(gistID)) - embedHTML = ('').format(gistID) + embedHTML = (f'') - output = '''{} - '''.format(embedHTML, rawGist) + output = f'''{embedHTML} + ''' return output, [] diff --git a/nikola/plugins/shortcode/listing.py b/nikola/plugins/shortcode/listing.py index d8cedbf83a..9dc901292e 100644 --- a/nikola/plugins/shortcode/listing.py +++ b/nikola/plugins/shortcode/listing.py @@ -71,7 +71,6 @@ def handler(self, fname, language='text', linenumbers=False, filename=None, site lexer = pygments.lexers.get_lexer_by_name(language) formatter = pygments.formatters.get_formatter_by_name( 'html', linenos=linenumbers) - output = '{0} ({2})' .format( - fname, target, src_label, src_target) + pygments.highlight(data, lexer, formatter) + output = f'{fname} ({src_label})' + pygments.highlight(data, lexer, formatter) return output, deps diff --git a/nikola/plugins/shortcode/thumbnail.py b/nikola/plugins/shortcode/thumbnail.py index 79c1bd7d82..80d80f7106 100644 --- a/nikola/plugins/shortcode/thumbnail.py +++ b/nikola/plugins/shortcode/thumbnail.py @@ -50,20 +50,20 @@ def handler(self, uri, alt=None, align=None, linktitle=None, title=None, imgclas figclass = '' if align and data: - figclass += ' align-{0}'.format(align) + figclass += f' align-{align}' elif align: - imgclass += ' align-{0}'.format(align) + imgclass += f' align-{align}' - output = '{output}{data}' return output, [] diff --git a/nikola/plugins/task/archive.py b/nikola/plugins/task/archive.py index 7a7cec2be2..7b57f9ea33 100644 --- a/nikola/plugins/task/archive.py +++ b/nikola/plugins/task/archive.py @@ -170,7 +170,7 @@ def provide_context_and_uptodate(self, classification, lang, node=None): datetime.date(int(hierarchy[0]), int(hierarchy[1]), int(hierarchy[2])), lang) else: - raise Exception("Cannot interpret classification {}!".format(repr(classification))) + raise Exception(f"Cannot interpret classification {repr(classification)}!") context = { "title": title, @@ -195,7 +195,7 @@ def provide_context_and_uptodate(self, classification, lang, node=None): flat_samelevel = self.archive_navigation[lang][nodelevel] idx = flat_samelevel.index(classification) if idx == -1: - raise Exception("Cannot find classification {0} in flat hierarchy!".format(classification)) + raise Exception(f"Cannot find classification {classification} in flat hierarchy!") previdx, nextidx = idx - 1, idx + 1 # If the previous index is -1, or the next index is 1, the previous/next archive does not exist. context["previous_archive"] = self.site.link('archive', flat_samelevel[previdx], lang) if previdx != -1 else None diff --git a/nikola/plugins/task/galleries.py b/nikola/plugins/task/galleries.py index bb4a458512..e7875d8580 100644 --- a/nikola/plugins/task/galleries.py +++ b/nikola/plugins/task/galleries.py @@ -97,7 +97,7 @@ def set_site(self, site): for source, dest in self.kw['gallery_folders'].items(): if source in appearing_paths or dest in appearing_paths: problem = source if source in appearing_paths else dest - utils.LOGGER.error("The gallery input or output folder '{0}' appears in more than one entry in GALLERY_FOLDERS, ignoring.".format(problem)) + utils.LOGGER.error(f"The gallery input or output folder '{problem}' appears in more than one entry in GALLERY_FOLDERS, ignoring.") continue appearing_paths.add(source) appearing_paths.add(dest) @@ -116,12 +116,12 @@ def _find_gallery_path(self, name): candidates = self.improper_gallery_links[name] if len(candidates) == 1: return candidates[0] - self.logger.error("Gallery name '{0}' is not unique! Possible output paths: {1}".format(name, candidates)) - raise RuntimeError("Gallery name '{0}' is not unique! Possible output paths: {1}".format(name, candidates)) + self.logger.error(f"Gallery name '{name}' is not unique! Possible output paths: {candidates}") + raise RuntimeError(f"Gallery name '{name}' is not unique! Possible output paths: {candidates}") else: - self.logger.error("Unknown gallery '{0}'!".format(name)) + self.logger.error(f"Unknown gallery '{name}'!") self.logger.info("Known galleries: " + str(list(self.proper_gallery_links.keys()))) - raise RuntimeError("Unknown gallery '{0}'!".format(name)) + raise RuntimeError(f"Unknown gallery '{name}'!") def gallery_path(self, name, lang): """Link to an image gallery's path. @@ -174,7 +174,7 @@ def gen_tasks(self): self.image_ext_list.extend(self.site.config.get('EXTRA_IMAGE_EXTENSIONS', [])) for k, v in self.site.GLOBAL_CONTEXT['template_hooks'].items(): - self.kw['||template_hooks|{0}||'.format(k)] = v.calculate_deps() + self.kw[f'||template_hooks|{k}||'] = v.calculate_deps() self.site.scan_posts() yield self.group_task() @@ -210,7 +210,7 @@ def gen_tasks(self): for lang in self.kw['translations']: # save navigation links as dependencies - self.kw['navigation_links|{0}'.format(lang)] = self.kw['global_context']['navigation_links'](lang) + self.kw[f'navigation_links|{lang}'] = self.kw['global_context']['navigation_links'](lang) # Create index.html for each language for lang in self.kw['translations']: @@ -475,8 +475,7 @@ def find_metadata(self, gallery, lang): else: return "", [], {}, {} - self.logger.debug("Using {0} for gallery {1}".format( - used_path, gallery)) + self.logger.debug(f"Using {used_path} for gallery {gallery}") with open(used_path, "r", encoding='utf-8-sig') as meta_file: if YAML is None: utils.req_missing(['ruamel.yaml'], 'use metadata.yml files for galleries') @@ -498,8 +497,7 @@ def find_metadata(self, gallery, lang): order.append(img_name) custom_metadata[img_name] = img else: - self.logger.error("no 'name:' for ({0}) in {1}".format( - img, used_path)) + self.logger.error(f"no 'name:' for ({img}) in {used_path}") return used_path, order, captions, custom_metadata def parse_index(self, gallery, input_folder, output_folder): diff --git a/nikola/plugins/task/gzip.py b/nikola/plugins/task/gzip.py index 9e8c80f4ff..9233f9b056 100644 --- a/nikola/plugins/task/gzip.py +++ b/nikola/plugins/task/gzip.py @@ -51,7 +51,7 @@ def process(self, task, prefix): 'file_dep': [], 'targets': [], 'actions': [], - 'basename': '{0}_gzip'.format(prefix), + 'basename': f'{prefix}_gzip', 'name': task.get('name').split(":", 1)[-1] + '.gz', 'clean': True, } diff --git a/nikola/plugins/task/listings.py b/nikola/plugins/task/listings.py index 9079815355..a13285d7f1 100644 --- a/nikola/plugins/task/listings.py +++ b/nikola/plugins/task/listings.py @@ -69,7 +69,7 @@ def set_site(self, site): for source, dest in self.kw['listings_folders'].items(): if source in appearing_paths or dest in appearing_paths: problem = source if source in appearing_paths else dest - utils.LOGGER.error("The listings input or output folder '{0}' appears in more than one entry in LISTINGS_FOLDERS, exiting.".format(problem)) + utils.LOGGER.error(f"The listings input or output folder '{problem}' appears in more than one entry in LISTINGS_FOLDERS, exiting.") continue appearing_paths.add(source) appearing_paths.add(dest) @@ -191,7 +191,7 @@ def render_listing(in_name, out_name, input_folder, output_folder, folders=[], f uptodate = {'c': self.site.GLOBAL_CONTEXT} for k, v in self.site.GLOBAL_CONTEXT['template_hooks'].items(): - uptodate['||template_hooks|{0}||'.format(k)] = v.calculate_deps() + uptodate[f'||template_hooks|{k}||'] = v.calculate_deps() for k in self.site._GLOBAL_CONTEXT_TRANSLATABLE: uptodate[k] = self.site.GLOBAL_CONTEXT[k](self.kw['default_lang']) @@ -303,14 +303,14 @@ def listing_path(self, namep, lang): # If the name shows up in this dict, we have to check for # ambiguities. if len(self.improper_input_file_mapping[name]) > 1: - utils.LOGGER.error("Using non-unique listing name '{0}', which maps to more than one listing name ({1})!".format(name, str(self.improper_input_file_mapping[name]))) + utils.LOGGER.error(f"Using non-unique listing name '{name}', which maps to more than one listing name ({str(self.improper_input_file_mapping[name])})!") return ["ERROR"] if len(self.site.config['LISTINGS_FOLDERS']) > 1: utils.LOGGER.warning("Using listings names in site.link() without input directory prefix while configuration's LISTINGS_FOLDERS has more than one entry.") name = list(self.improper_input_file_mapping[name])[0] break else: - utils.LOGGER.error("Unknown listing name {0}!".format(namep)) + utils.LOGGER.error(f"Unknown listing name {namep}!") return ["ERROR"] if not name.endswith(os.sep + self.site.config["INDEX_FILE"]): name += '.html' diff --git a/nikola/plugins/task/robots.py b/nikola/plugins/task/robots.py index 86fbf28c78..1c278c2ec5 100644 --- a/nikola/plugins/task/robots.py +++ b/nikola/plugins/task/robots.py @@ -59,11 +59,11 @@ def write_robots(): utils.LOGGER.info('Add "robots" to DISABLED_PLUGINS to disable this warning and robots.txt generation.') with io.open(robots_path, 'w+', encoding='utf8') as outf: - outf.write("Sitemap: {0}\n\n".format(sitemapindex_url)) + outf.write(f"Sitemap: {sitemapindex_url}\n\n") outf.write("User-Agent: *\n") if kw["robots_exclusions"]: for loc in kw["robots_exclusions"]: - outf.write("Disallow: {0}\n".format(loc)) + outf.write(f"Disallow: {loc}\n") outf.write("Host: {0}\n".format(urlparse(kw["base_url"]).netloc)) yield self.group_task() diff --git a/nikola/plugins/task/sitemap.py b/nikola/plugins/task/sitemap.py index 3777e6e86b..9422559ace 100644 --- a/nikola/plugins/task/sitemap.py +++ b/nikola/plugins/task/sitemap.py @@ -220,7 +220,7 @@ def robot_fetch(path): """Check if robots can fetch a file.""" for rule in kw["robots_exclusions"]: robot = robotparser.RobotFileParser() - robot.parse(["User-Agent: *", "Disallow: {0}".format(rule)]) + robot.parse(["User-Agent: *", f"Disallow: {rule}"]) if not robot.can_fetch("*", '/' + path): return False # not robot food return True diff --git a/nikola/plugins/task/taxonomies.py b/nikola/plugins/task/taxonomies.py index 8ebce3a792..e1c6cc76d3 100644 --- a/nikola/plugins/task/taxonomies.py +++ b/nikola/plugins/task/taxonomies.py @@ -49,7 +49,7 @@ def _generate_classification_overview_kw_context(self, taxonomy, lang): context, kw = taxonomy.provide_overview_context_and_uptodate(lang) context = copy(context) - context["kind"] = "{}_index".format(taxonomy.classification_name) + context["kind"] = f"{taxonomy.classification_name}_index" sorted_links = [] for other_lang in sorted(self.site.config['TRANSLATIONS'].keys()): if other_lang != lang: @@ -125,10 +125,10 @@ def acceptor(node): def _render_classification_overview(self, classification_name, template, lang, context, kw): # Prepare rendering - context["permalink"] = self.site.link("{}_index".format(classification_name), None, lang) + context["permalink"] = self.site.link(f"{classification_name}_index", None, lang) if "pagekind" not in context: context["pagekind"] = ["list", "tags_page"] - output_name = os.path.join(self.site.config['OUTPUT_FOLDER'], self.site.path('{}_index'.format(classification_name), None, lang)) + output_name = os.path.join(self.site.config['OUTPUT_FOLDER'], self.site.path(f'{classification_name}_index', None, lang)) blinker.signal('generate_classification_overview').send({ 'site': self.site, 'classification_name': classification_name, @@ -207,7 +207,7 @@ def _generate_classification_page_as_rss(self, taxonomy, classification, filtere 'file_dep': deps, 'targets': [output_name], 'actions': [(utils.generic_rss_renderer, - (lang, "{0} ({1})".format(blog_title, title) if blog_title != title else blog_title, + (lang, f"{blog_title} ({title})" if blog_title != title else blog_title, kw["site_url"], description, filtered_posts, output_name, kw["feed_teasers"], kw["feed_plain"], kw['feed_length'], feed_url, _enclosure, kw["feed_links_append_query"]))], diff --git a/nikola/post.py b/nikola/post.py index d7c1b9e38a..f54b701b3a 100644 --- a/nikola/post.py +++ b/nikola/post.py @@ -206,19 +206,19 @@ def _set_tags(self): self.post_status = status self.is_draft = True else: - LOGGER.warning(('The post "{0}" has the unknown status "{1}". ' - 'Valid values are "published", "featured", "private" and "draft".').format(self.source_path, status)) + LOGGER.warning((f'The post "{self.source_path}" has the unknown status "{status}". ' + 'Valid values are "published", "featured", "private" and "draft".')) if self.config['WARN_ABOUT_TAG_METADATA']: show_warning = False if 'draft' in [_.lower() for _ in self._tags[lang]]: - LOGGER.warning('The post "{0}" uses the "draft" tag.'.format(self.source_path)) + LOGGER.warning(f'The post "{self.source_path}" uses the "draft" tag.') show_warning = True if 'private' in self._tags[lang]: - LOGGER.warning('The post "{0}" uses the "private" tag.'.format(self.source_path)) + LOGGER.warning(f'The post "{self.source_path}" uses the "private" tag.') show_warning = True if 'mathjax' in self._tags[lang]: - LOGGER.warning('The post "{0}" uses the "mathjax" tag.'.format(self.source_path)) + LOGGER.warning(f'The post "{self.source_path}" uses the "mathjax" tag.') show_warning = True if show_warning: LOGGER.warning('It is suggested that you convert special tags to metadata and set ' @@ -229,14 +229,14 @@ def _set_tags(self): if self.config['USE_TAG_METADATA']: if 'draft' in [_.lower() for _ in self._tags[lang]]: self.is_draft = True - LOGGER.debug('The post "{0}" is a draft.'.format(self.source_path)) + LOGGER.debug(f'The post "{self.source_path}" is a draft.') self._tags[lang].remove('draft') self.post_status = 'draft' self.has_oldstyle_metadata_tags = True if 'private' in self._tags[lang]: self.is_private = True - LOGGER.debug('The post "{0}" is private.'.format(self.source_path)) + LOGGER.debug(f'The post "{self.source_path}" is private.') self._tags[lang].remove('private') self.post_status = 'private' self.has_oldstyle_metadata_tags = True @@ -269,11 +269,11 @@ def _set_translated_to(self): # If we don't have anything in translated_to, the file does not exist if not self.translated_to and os.path.isfile(self.source_path): - raise Exception(("Could not find translations for {}, check your " - "TRANSLATIONS_PATTERN").format(self.source_path)) + raise Exception((f"Could not find translations for {self.source_path}, check your " + "TRANSLATIONS_PATTERN")) elif not self.translated_to: - raise Exception(("Cannot use {} (not a file, perhaps a broken " - "symbolic link?)").format(self.source_path)) + raise Exception((f"Cannot use {self.source_path} (not a file, perhaps a broken " + "symbolic link?)")) def _set_folders(self, destination, destination_base): """Compose destination paths.""" @@ -299,10 +299,10 @@ def __migrate_section_to_category(self): # TODO: remove in v9 if 'section' in meta: if 'category' in meta: - LOGGER.warning("Post {0} has both 'category' and 'section' metadata. Section will be ignored.".format(self.source_path)) + LOGGER.warning(f"Post {self.source_path} has both 'category' and 'section' metadata. Section will be ignored.") else: meta['category'] = meta['section'] - LOGGER.info("Post {0} uses 'section' metadata, setting its value to 'category'".format(self.source_path)) + LOGGER.info(f"Post {self.source_path} uses 'section' metadata, setting its value to 'category'") # Handle CATEGORY_DESTPATH_AS_DEFAULT if 'category' not in meta and self.config['CATEGORY_DESTPATH_AS_DEFAULT']: @@ -358,7 +358,7 @@ def _set_date(self, default_metadata): self.date = to_datetime(self.meta[self.default_lang]['date'], self.config['__tzinfo__']) except ValueError: if not self.meta[self.default_lang]['date']: - msg = 'Missing date in file {}'.format(self.source_path) + msg = f'Missing date in file {self.source_path}' else: msg = "Invalid date '{0}' in file {1}".format(self.meta[self.default_lang]['date'], self.source_path) LOGGER.error(msg) @@ -400,7 +400,7 @@ def is_two_file(self, value): # Changing the value, this means you are transforming a 2-file # into a 1-file or viceversa. if value and not self.compiler.supports_metadata: - raise ValueError("Can't save metadata as 1-file using this compiler {}".format(self.compiler)) + raise ValueError(f"Can't save metadata as 1-file using this compiler {self.compiler}") for lang in self.translated_to: source = self.source(lang) meta = self.meta(lang) @@ -425,7 +425,7 @@ def __repr__(self): if vv: sub_meta[kk] = vv m.update(str(json.dumps(clean_meta, cls=utils.CustomEncoder, sort_keys=True)).encode('utf-8')) - return ''.format(self.source_path, m.hexdigest()) + return f'' def has_pretty_url(self, lang): """Check if this page has a pretty URL.""" @@ -613,7 +613,7 @@ def add_dependency(self, dependency, add='both', lang=None): If ``lang`` is not specified, this dependency is added for all languages. """ if add not in {'fragment', 'page', 'both'}: - raise Exception("Add parameter is '{0}', but must be either 'fragment', 'page', or 'both'.".format(add)) + raise Exception(f"Add parameter is '{add}', but must be either 'fragment', 'page', or 'both'.") if add == 'fragment' or add == 'both': self._dependency_file_fragment[lang].append((not isinstance(dependency, str), dependency)) if add == 'page' or add == 'both': @@ -742,8 +742,7 @@ def compile(self, lang): }) if self.publish_later: - LOGGER.info('{0} is scheduled to be published in the future ({1})'.format( - self.source_path, self.date)) + LOGGER.info(f'{self.source_path} is scheduled to be published in the future ({self.date})') def fragment_deps(self, lang): """Return a list of dependencies to build this post's fragment.""" @@ -824,7 +823,7 @@ def write_metadata(self, lang=None): if lang is None: lang = nikola.utils.LocaleBorg().current_lang if lang not in self.translated_to: - raise ValueError("Can't save post metadata to language [{}] it's not translated to.".format(lang)) + raise ValueError(f"Can't save post metadata to language [{lang}] it's not translated to.") source = self.source(lang) source_path = self.translated_source_path(lang) @@ -1124,7 +1123,7 @@ def get_metadata_from_file(source_path, post, config, lang, metadata_extractors_ with io.open(source_path, "r", encoding="utf-8-sig") as meta_file: source_text = meta_file.read() except (UnicodeDecodeError, UnicodeEncodeError): - msg = 'Error reading {0}: Nikola only supports UTF-8 files'.format(source_path) + msg = f'Error reading {source_path}: Nikola only supports UTF-8 files' LOGGER.error(msg) raise ValueError(msg) except Exception: # The file may not exist, for multilingual sites @@ -1247,7 +1246,7 @@ def hyphenate(dom, _lang): try: hyphenator = pyphen.Pyphen(lang=lang) except KeyError: - LOGGER.error("Cannot find hyphenation dictoniaries for {0} (from {1}).".format(lang, _lang)) + LOGGER.error(f"Cannot find hyphenation dictoniaries for {lang} (from {_lang}).") LOGGER.error("Pyphen cannot be installed to ~/.local (pip install --user).") if hyphenator is not None: for tag in ('p', 'li', 'span'): diff --git a/nikola/shortcodes.py b/nikola/shortcodes.py index aa3fd16c28..1be821783c 100644 --- a/nikola/shortcodes.py +++ b/nikola/shortcodes.py @@ -57,7 +57,7 @@ def _format_position(data, pos): else: col += 1 llb = '' - return "line {0}, column {1}".format(line + 1, col + 1) + return f"line {line + 1}, column {col + 1}" def _skip_whitespace(data, pos, must_be_nontrivial=False): @@ -68,7 +68,7 @@ def _skip_whitespace(data, pos, must_be_nontrivial=False): """ if must_be_nontrivial: if pos == len(data) or not data[pos].isspace(): - raise ParsingError("Expecting whitespace at {0}!".format(_format_position(data, pos))) + raise ParsingError(f"Expecting whitespace at {_format_position(data, pos)}!") while pos < len(data): if not data[pos].isspace(): break @@ -99,13 +99,13 @@ def _parse_quoted_string(data, start): value += data[pos + 1] pos += 2 else: - raise ParsingError("Unexpected end of data while escaping ({0})".format(_format_position(data, pos))) + raise ParsingError(f"Unexpected end of data while escaping ({_format_position(data, pos)})") elif (char == "'" or char == '"') and char == qc: return pos + 1, value else: value += char pos += 1 - raise ParsingError("Unexpected end of unquoted string (started at {0})!".format(_format_position(data, start))) + raise ParsingError(f"Unexpected end of unquoted string (started at {_format_position(data, start)})!") def _parse_unquoted_string(data, start, stop_at_equals): @@ -124,13 +124,13 @@ def _parse_unquoted_string(data, start, stop_at_equals): value += data[pos + 1] pos += 2 else: - raise ParsingError("Unexpected end of data while escaping ({0})".format(_format_position(data, pos))) + raise ParsingError(f"Unexpected end of data while escaping ({_format_position(data, pos)})") elif char.isspace(): break elif char == '=' and stop_at_equals: break elif char == "'" or char == '"': - raise ParsingError("Unexpected quotation mark in unquoted string ({0})".format(_format_position(data, pos))) + raise ParsingError(f"Unexpected quotation mark in unquoted string ({_format_position(data, pos)})") else: value += char pos += 1 @@ -156,7 +156,7 @@ def _parse_string(data, start, stop_at_equals=False, must_have_content=False): end, value = _parse_unquoted_string(data, start, stop_at_equals) has_content = len(value) > 0 if must_have_content and not has_content: - raise ParsingError("String starting at {0} must be non-empty!".format(_format_position(data, start))) + raise ParsingError(f"String starting at {_format_position(data, start)} must be non-empty!") next_is_equals = False if stop_at_equals and end + 1 < len(data): @@ -181,9 +181,9 @@ def _parse_shortcode_args(data, start, shortcode_name, start_pos): pos = _skip_whitespace(data, pos, must_be_nontrivial=True) except ParsingError: if not args and not kwargs: - raise ParsingError("Shortcode '{0}' starting at {1} is not terminated correctly with '%}}}}'!".format(shortcode_name, _format_position(data, start_pos))) + raise ParsingError(f"Shortcode '{shortcode_name}' starting at {_format_position(data, start_pos)} is not terminated correctly with '%}}}}'!") else: - raise ParsingError("Syntax error in shortcode '{0}' at {1}: expecting whitespace!".format(shortcode_name, _format_position(data, pos))) + raise ParsingError(f"Syntax error in shortcode '{shortcode_name}' at {_format_position(data, pos)}: expecting whitespace!") if pos == len(data): break # Check for end of shortcode @@ -200,7 +200,7 @@ def _parse_shortcode_args(data, start, shortcode_name, start_pos): # Store positional argument args.append(name) - raise ParsingError("Shortcode '{0}' starting at {1} is not terminated correctly with '%}}}}'!".format(shortcode_name, _format_position(data, start_pos))) + raise ParsingError(f"Shortcode '{shortcode_name}' starting at {_format_position(data, start_pos)} is not terminated correctly with '%}}}}'!") def _new_sc_id(): @@ -246,7 +246,7 @@ def extract_data_chunk(data): text.append(token[1]) return ''.join(text), data[1:] elif token[0] == 'SHORTCODE_END': # This is malformed - raise Exception('Closing unopened shortcode {}'.format(token[3])) + raise Exception(f'Closing unopened shortcode {token[3]}') text = [] tail = splitted @@ -285,7 +285,7 @@ def _split_shortcodes(data): name_end = _skip_nonwhitespace(data, name_start) name = data[name_start:name_end] if not name: - raise ParsingError("Syntax error: '{{{{%' must be followed by shortcode name ({0})!".format(_format_position(data, start))) + raise ParsingError(f"Syntax error: '{{{{%' must be followed by shortcode name ({_format_position(data, start)})!") # Finish shortcode if name[0] == '/': # This is a closing shortcode @@ -294,10 +294,10 @@ def _split_shortcodes(data): pos = end_start + 3 # Must be followed by '%}}' if pos > len(data) or data[end_start:pos] != '%}}': - raise ParsingError("Syntax error: '{{{{% /{0}' must be followed by ' %}}}}' ({1})!".format(name, _format_position(data, end_start))) + raise ParsingError(f"Syntax error: '{{{{% /{name}' must be followed by ' %}}}}' ({_format_position(data, end_start)})!") result.append(("SHORTCODE_END", data[start:pos], start, name)) elif name == '%}}': - raise ParsingError("Syntax error: '{{{{%' must be followed by shortcode name ({0})!".format(_format_position(data, start))) + raise ParsingError(f"Syntax error: '{{{{%' must be followed by shortcode name ({_format_position(data, start)})!") else: # This is an opening shortcode pos, args = _parse_shortcode_args(data, name_end, shortcode_name=name, start_pos=start) @@ -338,7 +338,7 @@ def apply_shortcodes(data, registry, site=None, filename=None, raise_exceptions= result.append(current[1]) pos += 1 elif current[0] == "SHORTCODE_END": - raise ParsingError("Found shortcode ending '{{{{% /{0} %}}}}' which isn't closing a started shortcode ({1})!".format(current[3], _format_position(data, current[2]))) + raise ParsingError(f"Found shortcode ending '{{{{% /{current[3]} %}}}}' which isn't closing a started shortcode ({_format_position(data, current[2])})!") elif current[0] == "SHORTCODE_START": name = current[3] # Check if we can find corresponding ending @@ -381,7 +381,7 @@ def apply_shortcodes(data, registry, site=None, filename=None, raise_exceptions= # Throw up raise if filename: - LOGGER.error("Shortcode error in file {0}: {1}".format(filename, e)) + LOGGER.error(f"Shortcode error in file {filename}: {e}") else: - LOGGER.error("Shortcode error: {0}".format(e)) + LOGGER.error(f"Shortcode error: {e}") sys.exit(1) diff --git a/nikola/utils.py b/nikola/utils.py index 660806f107..dae05dbbdb 100644 --- a/nikola/utils.py +++ b/nikola/utils.py @@ -154,13 +154,11 @@ def req_missing(names, purpose, python=True, optional=False): else: whatarethey_s = whatarethey_p = 'software' if len(names) == 1: - msg = 'In order to {0}, you must install the "{1}" {2}.'.format( - purpose, names[0], whatarethey_s) + msg = f'In order to {purpose}, you must install the "{names[0]}" {whatarethey_s}.' else: most = '", "'.join(names[:-1]) pnames = most + '" and "' + names[-1] - msg = 'In order to {0}, you must install the "{1}" {2}.'.format( - purpose, pnames, whatarethey_p) + msg = f'In order to {purpose}, you must install the "{pnames}" {whatarethey_p}.' if optional: LOGGER.warning(msg) @@ -195,7 +193,7 @@ def makedirs(path): return if os.path.exists(path): if not os.path.isdir(path): - raise OSError('Path {0} already exists and is not a folder.'.format(path)) + raise OSError(f'Path {path} already exists and is not a folder.') else: return try: @@ -317,7 +315,7 @@ def __str__(self): def __repr__(self): """Provide a representation for programmers.""" - return ''.format(self.name, self._inp) + return f'' def format(self, *args, **kwargs): """Format ALL the values in the setting the same way.""" @@ -481,11 +479,11 @@ def __hash__(self): def __str__(self): """Stringify a registry.""" - return ''.format(self._items) + return f'' def __repr__(self): """Provide the representation of a registry.""" - return ''.format(self.name) + return f'' class CustomEncoder(json.JSONEncoder): @@ -601,7 +599,7 @@ def get_theme_path_real(theme, themes_dirs) -> str: dir_name = pkg_resources_path('nikola', os.path.join('data', 'themes', theme)) if os.path.isdir(dir_name): return dir_name - raise Exception("Can't find theme '{0}'".format(theme)) + raise Exception(f"Can't find theme '{theme}'") def get_theme_path(theme): @@ -700,7 +698,7 @@ def __init__(self, lang, orig): def __str__(self): """Stringify the exception.""" - return 'cannot find language {0}'.format(self.lang) + return f'cannot find language {self.lang}' def load_messages(themes, translations, default_lang, themes_dirs): @@ -748,7 +746,7 @@ def load_messages(themes, translations, default_lang, themes_dirs): raise LanguageNotFoundError(lang, last_exception) for lang, status in completion_status.items(): if not status and lang not in INCOMPLETE_LANGUAGES_WARNED: - LOGGER.warning("Incomplete translation for language '{0}'.".format(lang)) + LOGGER.warning(f"Incomplete translation for language '{lang}'.") INCOMPLETE_LANGUAGES_WARNED.add(lang) return messages @@ -844,7 +842,7 @@ def slugify(value, lang=None, force=False): foo-bar """ if not isinstance(value, str): - raise ValueError("Not a unicode object: {0}".format(value)) + raise ValueError(f"Not a unicode object: {value}") if USE_SLUGIFY or force: # This is the standard state of slugify, which actually does some work. # It is the preferred style, especially for Western languages. @@ -897,9 +895,9 @@ def full_path_from_urlparse(parsed) -> str: """Given urlparse output, return the full path (with query and fragment).""" dst = parsed.path if parsed.query: - dst = "{0}?{1}".format(dst, parsed.query) + dst = f"{dst}?{parsed.query}" if parsed.fragment: - dst = "{0}#{1}".format(dst, parsed.fragment) + dst = f"{dst}#{parsed.fragment}" return dst # A very slightly safer version of zip.extractall that works on @@ -950,7 +948,7 @@ def to_datetime(value, tzinfo=None): value = value.replace(tzinfo=tzinfo) return value except Exception: - raise ValueError('Unrecognized date/time: {0!r}'.format(value)) + raise ValueError(f'Unrecognized date/time: {value!r}') def get_tzname(dt): @@ -996,7 +994,7 @@ def filter_matches(ext): if ext == key: return value else: - raise ValueError("Cannot find filter match for {0}".format(key)) + raise ValueError(f"Cannot find filter match for {key}") for target in task.get('targets', []): ext = os.path.splitext(target)[-1].lower() @@ -1203,7 +1201,7 @@ def initialize(cls, locales: Dict[str, str], initial_lang: str): locales: dict with custom locale name overrides. """ if not initial_lang: - raise ValueError("Unknown initial language {0}".format(initial_lang)) + raise ValueError(f"Unknown initial language {initial_lang}") cls.reset() cls.locales = locales cls.__initial_lang = initial_lang @@ -1311,7 +1309,7 @@ class ExtendedRSS2(rss.RSS2): def publish(self, handler): """Publish a feed.""" if self.xsl_stylesheet_href: - handler.processingInstruction("xml-stylesheet", 'type="text/xsl" href="{0}" media="all"'.format(self.xsl_stylesheet_href)) + handler.processingInstruction("xml-stylesheet", f'type="text/xsl" href="{self.xsl_stylesheet_href}" media="all"') super().publish(handler) def publish_extensions(self, handler): @@ -1390,8 +1388,8 @@ def demote_headers(doc, level=1): if before == after: continue - elements = doc.xpath('//h{}'.format(before)) - new_tag = 'h{}'.format(after) + elements = doc.xpath(f'//h{before}') + new_tag = f'h{after}' for element in elements: element.tag = new_tag @@ -1462,7 +1460,7 @@ def get_translation_candidate(config, path, lang): if l == lang: # Nothing to do return path elif lang == config['DEFAULT_LANG']: # Return untranslated path - return '{0}.{1}'.format(p, e) + return f'{p}.{e}' else: # Change lang and return return config['TRANSLATIONS_PATTERN'].format(path=p, ext=e, lang=lang) else: @@ -1509,9 +1507,9 @@ def write_metadata(data, metadata_format=None, comment_wrap=False, site=None, co extractor.check_requirements() return extractor.write_metadata(data, comment_wrap) elif extractor and metadata_format not in default_meta: - LOGGER.warning('Writing METADATA_FORMAT {} is not supported, using "nikola" format'.format(metadata_format)) + LOGGER.warning(f'Writing METADATA_FORMAT {metadata_format} is not supported, using "nikola" format') elif metadata_format not in default_meta: - LOGGER.warning('Unknown METADATA_FORMAT {}, using "nikola" format'.format(metadata_format)) + LOGGER.warning(f'Unknown METADATA_FORMAT {metadata_format}, using "nikola" format') if metadata_format == 'rest_docinfo': title = data['title'] @@ -1520,10 +1518,10 @@ def write_metadata(data, metadata_format=None, comment_wrap=False, site=None, co title, '=' * len(title), '' - ] + [':{0}: {1}'.format(k, v) for k, v in data.items() if v and k != 'title'] + [''] + ] + [f':{k}: {v}' for k, v in data.items() if v and k != 'title'] + [''] return '\n'.join(results) elif metadata_format == 'markdown_meta': - results = ['{0}: {1}'.format(k, v) for k, v in data.items() if v] + ['', ''] + results = [f'{k}: {v}' for k, v in data.items() if v] + ['', ''] return '\n'.join(results) else: # Nikola, default from nikola.metadata_extractors import DEFAULT_EXTRACTOR @@ -1551,10 +1549,10 @@ def bool_from_meta(meta, key, fallback=False, blank=None): def ask(query, default=None): """Ask a question.""" if default: - default_q = ' [{0}]'.format(default) + default_q = f' [{default}]' else: default_q = '' - inp = input("{query}{default_q}: ".format(query=query, default_q=default_q)).strip() + inp = input(f"{query}{default_q}: ").strip() if inp or default is None: return inp else: @@ -1569,7 +1567,7 @@ def ask_yesno(query, default=None): default_q = ' [Y/n]' elif default is False: default_q = ' [y/N]' - inp = input("{query}{default_q} ".format(query=query, default_q=default_q)).strip() + inp = input(f"{query}{default_q} ").strip() if inp: return inp.lower().startswith('y') elif default is not None: @@ -1676,7 +1674,7 @@ def options2docstring(name, options): """Translate options to a docstring.""" result = ['Function wrapper for command %s' % name, 'arguments:'] for opt in options: - result.append('{0} type {1} default {2}'.format(opt.name, opt.type.__name__, opt.default)) + result.append(f'{opt.name} type {opt.type.__name__} default {opt.default}') return '\n'.join(result) @@ -1712,7 +1710,7 @@ def wrap(self, source, *args): style = '; '.join(style) classes = ' '.join(self.nclasses) - yield 0, ('
')
+        yield 0, (f'
')
         for tup in source:
             yield tup
         yield 0, '
' @@ -1768,7 +1766,7 @@ def adjust_name_for_index_path_list(path_list, i, displayed_i, lang, site, force for entry in path_schema: path_list.append(entry.format(number=displayed_i, old_number=i, index_file=index_file)) else: - path_list[-1] = '{0}-{1}{2}'.format(os.path.splitext(path_list[-1])[0], i, extension) + path_list[-1] = f'{os.path.splitext(path_list[-1])[0]}-{i}{extension}' return path_list @@ -1808,8 +1806,8 @@ def create_redirect(src, dst): fd.write('\n\n\n' 'Redirecting...\n\n\n\n\n

Page moved ' - 'here.

\n'.format(dst)) + f'url={dst}">\n\n\n

Page moved ' + f'here.

\n') def colorize_str_from_base_color(string, base_color): @@ -1870,7 +1868,7 @@ def dns_sd(port, inet6): import avahi import dbus inet = avahi.PROTO_INET6 if inet6 else avahi.PROTO_INET - name = "{0}'s Nikola Server on {1}".format(os.getlogin(), socket.gethostname()) + name = f"{os.getlogin()}'s Nikola Server on {socket.gethostname()}" bus = dbus.SystemBus() bus_server = dbus.Interface(bus.get_object(avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER), @@ -2158,16 +2156,16 @@ def read_from_config(self, site, basename, posts_per_classification_per_language ``posts_per_classification_per_language``. """ # Add translations - for record in site.config.get('{}_TRANSLATIONS'.format(basename), []): + for record in site.config.get(f'{basename}_TRANSLATIONS', []): self.add_translation(record) # Add default translations - if site.config.get('{}_TRANSLATIONS_ADD_DEFAULTS'.format(basename), add_defaults_default): + if site.config.get(f'{basename}_TRANSLATIONS_ADD_DEFAULTS', add_defaults_default): self.add_defaults(posts_per_classification_per_language) # Use blinker to inform interested parties (plugins) that they can add # translations themselves args = {'translation_manager': self, 'site': site, 'posts_per_classification_per_language': posts_per_classification_per_language} - signal('{}_translations_config'.format(basename.lower())).send(args) + signal(f'{basename.lower()}_translations_config').send(args) def base_path_from_siteuri(siteuri: str) -> str: diff --git a/scripts/import_po.py b/scripts/import_po.py index 3f2c984294..90b8f2b64d 100755 --- a/scripts/import_po.py +++ b/scripts/import_po.py @@ -19,7 +19,7 @@ lang = os.path.splitext(os.path.basename(fname))[0].lower() lang = lang.replace('@', '_') outf = os.path.join('nikola', 'data', 'themes', 'base', - 'messages', 'messages_{0}.py'.format(lang)) + 'messages', f'messages_{lang}.py') po = polib.pofile(fname) lines = """# -*- encoding:utf-8 -*- \"\"\"Autogenerated file, do not edit. Submit translations on Transifex.\"\"\" @@ -27,7 +27,7 @@ MESSAGES = {""".splitlines() lines2 = [] for entry in po: - lines2.append(' "{0}": "{1}",'. format(entry.msgid, entry.msgstr)) + lines2.append(f' "{entry.msgid}": "{entry.msgstr}",') lines.extend(sorted(lines2)) lines.append("}\n") print("Generating:", outf) diff --git a/scripts/jinjify.py b/scripts/jinjify.py index 4043ddadca..67b41c489a 100755 --- a/scripts/jinjify.py +++ b/scripts/jinjify.py @@ -76,7 +76,7 @@ def jinjify(in_theme, out_theme): try: lookup.parse(source) except Exception as e: - error("Syntax error in {0}:{1}".format(out_template, e.lineno)) + error(f"Syntax error in {out_template}:{e.lineno}") parent = os.path.basename(in_theme.rstrip('/')) child = os.path.basename(out_theme.rstrip('/')) @@ -106,7 +106,7 @@ def jinjify(in_theme, out_theme): def error(msg): - print("\033[1;31mERROR: {0}\033[0m".format(msg)) + print(f"\033[1;31mERROR: {msg}\033[0m") def mako2jinja(input_file): @@ -239,9 +239,9 @@ def jinjify_shortcodes(in_dir, out_dir): def usage(): - print("Usage: python {} [in-dir] [out-dir]".format(sys.argv[0])) + print(f"Usage: python {sys.argv[0]} [in-dir] [out-dir]") print("OR") - print("Usage: python {} [in-file] [out-file]".format(sys.argv[0])) + print(f"Usage: python {sys.argv[0]} [in-file] [out-file]") if __name__ == "__main__": if len(sys.argv) == 1: @@ -251,7 +251,7 @@ def usage(): ('nikola/data/themes/bootstrap4', 'nikola/data/themes/bootstrap4-jinja'), ('nikola/data/themes/bootblog4', 'nikola/data/themes/bootblog4-jinja'), ): - print(' {0} -> {1}'.format(m, j)) + print(f' {m} -> {j}') jinjify(m, j) jinjify_shortcodes('nikola/data/shortcodes/mako', 'nikola/data/shortcodes/jinja') elif len(sys.argv) != 3: diff --git a/scripts/langstatus.py b/scripts/langstatus.py index c31067d221..6be7625dbd 100755 --- a/scripts/langstatus.py +++ b/scripts/langstatus.py @@ -15,8 +15,8 @@ lang = file.split('_', 1)[1][:-3] exist.append(lang) if lang in used: - print('{0}: found'.format(lang)) + print(f'{lang}: found') elif os.path.islink(file): - print('\x1b[1;1m\x1b[1;30m{0}: symlink\x1b[0m'.format(lang)) + print(f'\x1b[1;1m\x1b[1;30m{lang}: symlink\x1b[0m') else: - print('\x1b[1;1m\x1b[1;31m{0}: NOT found\x1b[0m'.format(lang)) + print(f'\x1b[1;1m\x1b[1;31m{lang}: NOT found\x1b[0m') diff --git a/tests/integration/helper.py b/tests/integration/helper.py index c120721fdc..aee45dd887 100644 --- a/tests/integration/helper.py +++ b/tests/integration/helper.py @@ -19,11 +19,11 @@ def create_simple_post(directory, filename, title_slug, text='', date='2013-03-0 text_processed = '\n' + text if text else '' with io.open(path, "w+", encoding="utf8") as outf: outf.write( - """ -.. title: {0} -.. slug: {0} -.. date: {1} -{2}""".format(title_slug, date, text_processed) + f""" +.. title: {title_slug} +.. slug: {title_slug} +.. date: {date} +{text_processed}""" ) diff --git a/tests/test_metadata_extractors.py b/tests/test_metadata_extractors.py index 58dd7b59c3..a47be23f7f 100644 --- a/tests/test_metadata_extractors.py +++ b/tests/test_metadata_extractors.py @@ -39,8 +39,8 @@ def test_builtin_extractors_rest( ): is_two_files = filecount == 2 - source_filename = "f-rest-{0}-{1}.rst".format(filecount, format_lc) - metadata_filename = "f-rest-{0}-{1}.meta".format(filecount, format_lc) + source_filename = f"f-rest-{filecount}-{format_lc}.rst" + metadata_filename = f"f-rest-{filecount}-{format_lc}.meta" source_path = os.path.join(testfiledir, source_filename) metadata_path = os.path.join(testfiledir, metadata_filename) post = FakePost(source_path, metadata_path, {}, None, metadata_extractors_by) @@ -54,8 +54,8 @@ def test_builtin_extractors_rest( assert meta assert extractor is metadata_extractors_by["name"][format_lc] - assert meta["title"] == "T: reST, {0}, {1}".format(filecount, format_friendly) - assert meta["slug"] == "s-rest-{0}-{1}".format(filecount, format_lc) + assert meta["title"] == f"T: reST, {filecount}, {format_friendly}" + assert meta["slug"] == f"s-rest-{filecount}-{format_lc}" assert expected in meta["tags"] assert unexpected not in meta["tags"] assert "meta" in meta["tags"] @@ -73,8 +73,8 @@ def test_nikola_meta_markdown( ): is_two_files = filecount == 2 - source_filename = "f-markdown-{0}-nikola.md".format(filecount) - metadata_filename = "f-markdown-{0}-nikola.meta".format(filecount) + source_filename = f"f-markdown-{filecount}-nikola.md" + metadata_filename = f"f-markdown-{filecount}-nikola.meta" source_path = os.path.join(testfiledir, source_filename) metadata_path = os.path.join(testfiledir, metadata_filename) post = FakePost(source_path, metadata_path, {}, None, metadata_extractors_by) @@ -86,8 +86,8 @@ def test_nikola_meta_markdown( meta, extractor = get_meta(post, None) assert extractor is metadata_extractors_by["name"]["nikola"] - assert meta["title"] == "T: Markdown, {0}, Nikola".format(filecount) - assert meta["slug"] == "s-markdown-{0}-nikola".format(filecount) + assert meta["title"] == f"T: Markdown, {filecount}, Nikola" + assert meta["slug"] == f"s-markdown-{filecount}-nikola" assert expected in meta["tags"] assert unexpected not in meta["tags"] assert "meta" in meta["tags"] @@ -108,10 +108,10 @@ def test_nikola_meta_markdown( def test_compiler_metadata( metadata_extractors_by, testfiledir, compiler, fileextension, compiler_lc, name ): - source_filename = "f-{0}-1-compiler.{1}".format(compiler_lc, fileextension) - metadata_filename = "f-{0}-1-compiler.meta".format(compiler_lc) - title = "T: {0}, 1, compiler".format(name) - slug = "s-{0}-1-compiler".format(compiler_lc) + source_filename = f"f-{compiler_lc}-1-compiler.{fileextension}" + metadata_filename = f"f-{compiler_lc}-1-compiler.meta" + title = f"T: {name}, 1, compiler" + slug = f"s-{compiler_lc}-1-compiler" source_path = os.path.join(testfiledir, source_filename) metadata_path = os.path.join(testfiledir, metadata_filename) diff --git a/tests/test_rst_compiler.py b/tests/test_rst_compiler.py index 88638f7ac6..c74b2a83bf 100644 --- a/tests/test_rst_compiler.py +++ b/tests/test_rst_compiler.py @@ -210,7 +210,7 @@ def assert_html_contains(html, element, attributes=None, text=None): try: tag = next(html_doc.iter(element)) except StopIteration: - raise Exception("<{0}> not in {1}".format(element, html)) + raise Exception(f"<{element}> not in {html}") if attributes: arg_attrs = set(attributes.items()) diff --git a/tests/test_shortcodes.py b/tests/test_shortcodes.py index 83831767fe..460d83e365 100644 --- a/tests/test_shortcodes.py +++ b/tests/test_shortcodes.py @@ -189,7 +189,7 @@ def register_shortcode(self, name, f): def noargs(site, data="", lang=""): - return "noargs {0} success!".format(data) + return f"noargs {data} success!" def arg(*args, **kwargs): @@ -197,4 +197,4 @@ def arg(*args, **kwargs): kwargs.pop("site") data = kwargs.pop("data") kwargs.pop("lang") - return "arg {0}/{1}/{2}".format(args, sorted(kwargs.items()), data) + return f"arg {args}/{sorted(kwargs.items())}/{data}" From a6a285299b2b53baba858c32fbece3996a9ae088 Mon Sep 17 00:00:00 2001 From: Arun Persaud Date: Sun, 2 Mar 2025 20:09:34 -0800 Subject: [PATCH 2/3] updated CHANGES.txt --- CHANGES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.txt b/CHANGES.txt index f369a17f0c..967914eb19 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -27,6 +27,7 @@ Other ----- * Changed filter for tidy from ``tidy5`` to ``tidy``. +* Converted from .format() to f-strings New in v8.3.1 ============= From 614dfffa9bfb3e99aeaf197b62923f04dba5bf41 Mon Sep 17 00:00:00 2001 From: Arun Persaud Date: Sat, 17 May 2025 08:52:19 -0700 Subject: [PATCH 3/3] remove additional whitespace to fix flake8 errors --- nikola/plugins/compile/rest/listing.py | 2 +- nikola/plugins/shortcode/listing.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nikola/plugins/compile/rest/listing.py b/nikola/plugins/compile/rest/listing.py index 7b0fac77c9..b954a9bfaf 100644 --- a/nikola/plugins/compile/rest/listing.py +++ b/nikola/plugins/compile/rest/listing.py @@ -216,7 +216,7 @@ def run(self): src_target = urlunsplit(("link", 'listing_source', fpath.replace('\\', '/'), '', '')) src_label = self.site.MESSAGES('Source') generated_nodes = ( - [core.publish_doctree(f'`{_fname} <{target}>`_ `({src_label}) <{src_target}>`_' )[0]]) + [core.publish_doctree(f'`{_fname} <{target}>`_ `({src_label}) <{src_target}>`_')[0]]) generated_nodes += self.get_code_from_file(fileobject) return generated_nodes diff --git a/nikola/plugins/shortcode/listing.py b/nikola/plugins/shortcode/listing.py index 9dc901292e..4592c7adfa 100644 --- a/nikola/plugins/shortcode/listing.py +++ b/nikola/plugins/shortcode/listing.py @@ -71,6 +71,6 @@ def handler(self, fname, language='text', linenumbers=False, filename=None, site lexer = pygments.lexers.get_lexer_by_name(language) formatter = pygments.formatters.get_formatter_by_name( 'html', linenos=linenumbers) - output = f'{fname} ({src_label})' + pygments.highlight(data, lexer, formatter) + output = f'{fname} ({src_label})' + pygments.highlight(data, lexer, formatter) return output, deps