458 lines
14 KiB
Python
458 lines
14 KiB
Python
|
|
#!/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 <subcommand>' 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()
|