#!/usr/bin/env python import fileinput import fnmatch import os import re import sys from argparse import ArgumentParser from difflib import unified_diff from django.core.management import ManagementUtility CURRENT_PYTHON = sys.version_info[:2] REQUIRED_PYTHON = (3, 7) if CURRENT_PYTHON < REQUIRED_PYTHON: sys.stderr.write( "This version of Wagtail requires Python {}.{} or above - you are running {}.{}\n".format( *(REQUIRED_PYTHON + CURRENT_PYTHON) ) ) sys.exit(1) def pluralize(value, arg="s"): return "" if value == 1 else arg class Command: description = None def create_parser(self, command_name=None): if command_name is None: prog = None else: # hack the prog name as reported to ArgumentParser to include the command prog = f"{prog_name()} {command_name}" parser = ArgumentParser( description=getattr(self, "description", None), add_help=False, prog=prog ) self.add_arguments(parser) return parser def add_arguments(self, parser): pass def print_help(self, command_name): parser = self.create_parser(command_name=command_name) parser.print_help() def execute(self, argv): parser = self.create_parser() options = parser.parse_args(sys.argv[2:]) options_dict = vars(options) self.run(**options_dict) class CreateProject(Command): description = "Creates the directory structure for a new Wagtail project." def __init__(self): self.default_template_path = self.get_default_template_path() def add_arguments(self, parser): parser.add_argument("project_name", help="Name for your Wagtail project") parser.add_argument( "dest_dir", nargs="?", help="Destination directory inside which to create the project", ) parser.add_argument( "--template", help="The path or URL to load the template from.", default=self.default_template_path, ) def get_default_template_path(self): import wagtail wagtail_path = os.path.dirname(wagtail.__file__) default_template_path = os.path.join(wagtail_path, "project_template") return default_template_path def run(self, project_name=None, dest_dir=None, **options): # Make sure given name is not already in use by another python package/module. try: __import__(project_name) except ImportError: pass else: sys.exit( "'%s' conflicts with the name of an existing " "Python module and cannot be used as a project " "name. Please try another name." % project_name ) template_name = options["template"] if template_name == self.default_template_path: template_name = "the default Wagtail template" print( # noqa: T201 "Creating a Wagtail project called %(project_name)s using %(template_name)s" % {"project_name": project_name, "template_name": template_name} ) # Call django-admin startproject utility_args = [ "django-admin", "startproject", "--template=" + options["template"], "--ext=html,rst", "--name=Dockerfile", project_name, ] if dest_dir: utility_args.append(dest_dir) utility = ManagementUtility(utility_args) utility.execute() print( # noqa: T201 "Success! %(project_name)s has been created" % {"project_name": project_name} ) class UpdateModulePaths(Command): description = "Update a Wagtail project tree to use Wagtail 2.x module paths" REPLACEMENTS = [ # Added in Wagtail 2.0 (re.compile(r"\bwagtail\.wagtailcore\b"), "wagtail"), (re.compile(r"\bwagtail\.wagtailadmin\b"), "wagtail.admin"), (re.compile(r"\bwagtail\.wagtaildocs\b"), "wagtail.documents"), (re.compile(r"\bwagtail\.wagtailembeds\b"), "wagtail.embeds"), (re.compile(r"\bwagtail\.wagtailimages\b"), "wagtail.images"), (re.compile(r"\bwagtail\.wagtailsearch\b"), "wagtail.search"), (re.compile(r"\bwagtail\.wagtailsites\b"), "wagtail.sites"), (re.compile(r"\bwagtail\.wagtailsnippets\b"), "wagtail.snippets"), (re.compile(r"\bwagtail\.wagtailusers\b"), "wagtail.users"), (re.compile(r"\bwagtail\.wagtailforms\b"), "wagtail.contrib.forms"), (re.compile(r"\bwagtail\.wagtailredirects\b"), "wagtail.contrib.redirects"), ( re.compile(r"\bwagtail\.contrib\.wagtailfrontendcache\b"), "wagtail.contrib.frontend_cache", ), ( re.compile(r"\bwagtail\.contrib\.wagtailroutablepage\b"), "wagtail.contrib.routable_page", ), ( re.compile(r"\bwagtail\.contrib\.wagtailsearchpromotions\b"), "wagtail.contrib.search_promotions", ), ( re.compile(r"\bwagtail\.contrib\.wagtailsitemaps\b"), "wagtail.contrib.sitemaps", ), ( re.compile(r"\bwagtail\.contrib\.wagtailstyleguide\b"), "wagtail.contrib.styleguide", ), # Added in Wagtail 3.0 (re.compile(r"\bwagtail\.tests\b"), "wagtail.test"), (re.compile(r"\bwagtail\.core\.utils\b"), "wagtail.coreutils"), (re.compile(r"\bwagtail\.core\b"), "wagtail"), (re.compile(r"\bwagtail\.admin\.edit_handlers\b"), "wagtail.admin.panels"), ( re.compile(r"\bwagtail\.contrib\.forms\.edit_handlers\b"), "wagtail.contrib.forms.panels", ), ] def add_arguments(self, parser): parser.add_argument("root_path", nargs="?", help="Path to your project's root") parser.add_argument( "--list", action="store_true", dest="list_files", help="Show the list of files to change, without modifying them", ) parser.add_argument( "--diff", action="store_true", help="Show the changes that would be made, without modifying the files", ) parser.add_argument( "--ignore-dir", action="append", dest="ignored_dirs", metavar="NAME", help="Ignore files in this directory", ) parser.add_argument( "--ignore-file", action="append", dest="ignored_patterns", metavar="NAME", help="Ignore files with this name (supports wildcards)", ) def run( self, root_path=None, list_files=False, diff=False, ignored_dirs=None, ignored_patterns=None, ): if root_path is None: root_path = os.getcwd() absolute_ignored_dirs = [ os.path.abspath(dir_path) + os.sep for dir_path in (ignored_dirs or []) ] if ignored_patterns is None: ignored_patterns = [] checked_file_count = 0 changed_file_count = 0 for dirpath, dirnames, filenames in os.walk(root_path): dirpath_with_slash = os.path.abspath(dirpath) + os.sep if any( dirpath_with_slash.startswith(ignored_dir) for ignored_dir in absolute_ignored_dirs ): continue for filename in filenames: if not filename.lower().endswith(".py"): continue if any( fnmatch.fnmatch(filename, pattern) for pattern in ignored_patterns ): continue path = os.path.join(dirpath, filename) relative_path = os.path.relpath(path, start=root_path) checked_file_count += 1 if diff: change_count = self._show_diff(path, relative_path=relative_path) else: if list_files: change_count = self._count_changes(path) else: # actually update change_count = self._rewrite_file(path) if change_count: print( # noqa: T201 "%s - %d change%s" % (relative_path, change_count, pluralize(change_count)) ) if change_count: changed_file_count += 1 if diff or list_files: print( # noqa: T201 "\nChecked %d .py file%s, %d file%s to update." % ( checked_file_count, pluralize(checked_file_count), changed_file_count, pluralize(changed_file_count), ) ) else: print( # noqa: T201 "\nChecked %d .py file%s, %d file%s updated." % ( checked_file_count, pluralize(checked_file_count), changed_file_count, pluralize(changed_file_count), ) ) def _rewrite_line(self, line): for pattern, repl in self.REPLACEMENTS: line = re.sub(pattern, repl, line) return line def _show_diff(self, filename, relative_path=None): change_count = 0 found_unicode_error = False original = [] updated = [] with open(filename, mode="rb") as f: for raw_original_line in f: try: original_line = raw_original_line.decode("utf-8") except UnicodeDecodeError: found_unicode_error = True # retry decoding as utf-8, mangling invalid bytes so that we have a usable string to use the diff line = original_line = raw_original_line.decode( "utf-8", errors="replace" ) else: line = self._rewrite_line(original_line) original.append(original_line) updated.append(line) if line != original_line: change_count += 1 if found_unicode_error: sys.stderr.write( "Warning - %s is not a valid UTF-8 file. Lines with decode errors have been ignored\n" % filename ) if change_count: relative_path = relative_path or filename sys.stdout.writelines( unified_diff( original, updated, fromfile="%s:before" % relative_path, tofile="%s:after" % relative_path, ) ) return change_count def _count_changes(self, filename): change_count = 0 found_unicode_error = False with open(filename, mode="rb") as f: for raw_original_line in f: try: original_line = raw_original_line.decode("utf-8") except UnicodeDecodeError: found_unicode_error = True else: line = self._rewrite_line(original_line) if line != original_line: change_count += 1 if found_unicode_error: sys.stderr.write( "Warning - %s is not a valid UTF-8 file. Lines with decode errors have been ignored\n" % filename ) return change_count def _rewrite_file(self, filename): change_count = 0 found_unicode_error = False with fileinput.FileInput(filename, inplace=True, mode="rb") as f: for raw_original_line in f: try: original_line = raw_original_line.decode("utf-8") except UnicodeDecodeError: sys.stdout.write(raw_original_line) found_unicode_error = True else: line = self._rewrite_line(original_line) if CURRENT_PYTHON >= (3, 8): sys.stdout.write(line.encode("utf-8")) else: # Python 3.7 opens the output stream in text mode, so write the line back as # text rather than bytes: # https://github.com/python/cpython/commit/be6dbfb43b89989ccc83fbc4c5234f50f44c47ad sys.stdout.write(line) if line != original_line: change_count += 1 if found_unicode_error: sys.stderr.write( "Warning - %s is not a valid UTF-8 file. Lines with decode errors have been ignored\n" % filename ) return change_count class Version(Command): description = "List which version of Wagtail you are using" def run(self): import wagtail version = wagtail.get_version(wagtail.VERSION) print(f"You are using Wagtail {version}") # noqa: T201 COMMANDS = { "start": CreateProject(), "updatemodulepaths": UpdateModulePaths(), "--version": Version(), } def prog_name(): return os.path.basename(sys.argv[0]) def help_index(): print( # noqa: T201 "Type '%s help ' for help on a specific subcommand.\n" % prog_name() ) print("Available subcommands:\n") # NOQA: T201 for name, cmd in sorted(COMMANDS.items()): print(f" {name.ljust(20)}{cmd.description}") # NOQA: T201 def unknown_command(command): print("Unknown command: '%s'" % command) # NOQA: T201 print("Type '%s help' for usage." % prog_name()) # NOQA: T201 sys.exit(1) def main(): try: command_name = sys.argv[1] except IndexError: help_index() return if command_name == "help": try: help_command_name = sys.argv[2] except IndexError: help_index() return try: command = COMMANDS[help_command_name] except KeyError: unknown_command(help_command_name) return command.print_help(help_command_name) return try: command = COMMANDS[command_name] except KeyError: unknown_command(command_name) return command.execute(sys.argv) if __name__ == "__main__": main()