aboutsummaryrefslogtreecommitdiffstats
path: root/nikola/__main__.py
diff options
context:
space:
mode:
authorLibravatarUnit 193 <unit193@unit193.net>2021-02-03 19:17:00 -0500
committerLibravatarUnit 193 <unit193@unit193.net>2021-02-03 19:17:00 -0500
commit3a0d66f07b112b6d2bdc2b57bbf717a89a351ce6 (patch)
treea7cf56282e54f05785243bc1e903d6594f2c06ba /nikola/__main__.py
parent787b97a4cb24330b36f11297c6d3a7a473a907d0 (diff)
New upstream version 8.1.2.upstream/8.1.2
Diffstat (limited to 'nikola/__main__.py')
-rw-r--r--nikola/__main__.py185
1 files changed, 125 insertions, 60 deletions
diff --git a/nikola/__main__.py b/nikola/__main__.py
index 2aa63f4..8330e67 100644
--- a/nikola/__main__.py
+++ b/nikola/__main__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2015 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,37 +26,37 @@
"""The main function of Nikola."""
-from __future__ import print_function, unicode_literals
-from collections import defaultdict
+import importlib.util
import os
import shutil
-try:
- import readline # NOQA
-except ImportError:
- pass # This is only so raw_input/input does nicer things if it's available
import sys
+import textwrap
import traceback
+import doit.cmd_base
+from collections import defaultdict
-from doit.loader import generate_tasks
-from doit.cmd_base import TaskLoader
-from doit.reporter import ExecutedOnlyReporter
-from doit.doit_cmd import DoitMain
+from blinker import signal
+from doit.cmd_auto import Auto as DoitAuto
+from doit.cmd_base import TaskLoader, _wrap
+from doit.cmd_clean import Clean as DoitClean
+from doit.cmd_completion import TabCompletion
from doit.cmd_help import Help as DoitHelp
from doit.cmd_run import Run as DoitRun
-from doit.cmd_clean import Clean as DoitClean
-from doit.cmd_auto import Auto as DoitAuto
-from logbook import NullHandler
-from blinker import signal
+from doit.doit_cmd import DoitMain
+from doit.loader import generate_tasks
+from doit.reporter import ExecutedOnlyReporter
from . import __version__
-from .plugin_categories import Command
from .nikola import Nikola
-from .utils import sys_decode, sys_encode, get_root_dir, req_missing, LOGGER, STRICT_HANDLER, STDERR_HANDLER, ColorfulStderrHandler
+from .plugin_categories import Command
+from .log import configure_logging, LOGGER, ColorfulFormatter, LoggingMode
+from .utils import get_root_dir, req_missing, sys_decode
+
+try:
+ import readline # NOQA
+except ImportError:
+ pass # This is only so raw_input/input does nicer things if it's available
-if sys.version_info[0] == 3:
- import importlib.machinery
-else:
- import imp
config = {}
@@ -67,10 +67,10 @@ _RETURN_DOITNIKOLA = False
def main(args=None):
"""Run Nikola."""
colorful = False
- if sys.stderr.isatty() and os.name != 'nt':
+ if sys.stderr.isatty() and os.name != 'nt' and os.getenv('NIKOLA_MONO') is None and os.getenv('TERM') != 'dumb':
colorful = True
- ColorfulStderrHandler._colorful = colorful
+ ColorfulFormatter._colorful = colorful
if args is None:
args = sys.argv[1:]
@@ -79,29 +79,24 @@ def main(args=None):
args = [sys_decode(arg) for arg in args]
conf_filename = 'conf.py'
- conf_filename_bytes = b'conf.py'
conf_filename_changed = False
for index, arg in enumerate(args):
if arg[:7] == '--conf=':
del args[index]
del oargs[index]
conf_filename = arg[7:]
- conf_filename_bytes = sys_encode(arg[7:])
conf_filename_changed = True
break
quiet = False
- strict = False
if len(args) > 0 and args[0] == 'build' and '--strict' in args:
- LOGGER.notice('Running in strict mode')
- STRICT_HANDLER.push_application()
- strict = True
- if len(args) > 0 and args[0] == 'build' and '-q' in args or '--quiet' in args:
- NullHandler().push_application()
+ LOGGER.info('Running in strict mode')
+ configure_logging(LoggingMode.STRICT)
+ elif len(args) > 0 and args[0] == 'build' and '-q' in args or '--quiet' in args:
+ configure_logging(LoggingMode.QUIET)
quiet = True
- if not quiet and not strict:
- NullHandler().push_application()
- STDERR_HANDLER[0].push_application()
+ else:
+ configure_logging()
global config
@@ -118,20 +113,22 @@ def main(args=None):
os.chdir(root)
# Help and imports don't require config, but can use one if it exists
needs_config_file = (argname != 'help') and not argname.startswith('import_')
+ LOGGER.debug("Website root: %r", root)
else:
needs_config_file = False
- sys.path.append('')
+ sys.path.insert(0, os.path.dirname(conf_filename))
try:
- if sys.version_info[0] == 3:
- loader = importlib.machinery.SourceFileLoader("conf", conf_filename)
- conf = loader.load_module()
- else:
- conf = imp.load_source("conf", conf_filename_bytes)
+ spec = importlib.util.spec_from_file_location("conf", conf_filename)
+ conf = importlib.util.module_from_spec(spec)
+ # Preserve caching behavior of `import conf` if the filename matches
+ if os.path.splitext(os.path.basename(conf_filename))[0] == "conf":
+ sys.modules["conf"] = conf
+ spec.loader.exec_module(conf)
config = conf.__dict__
except Exception:
if os.path.exists(conf_filename):
- msg = traceback.format_exc(0)
+ msg = traceback.format_exc()
LOGGER.error('"{0}" cannot be parsed.\n{1}'.format(conf_filename, msg))
return 1
elif needs_config_file and conf_filename_changed:
@@ -154,7 +151,7 @@ def main(args=None):
req_missing(['freezegun'], 'perform invariant builds')
if config:
- if os.path.exists('plugins') and not os.path.exists('plugins/__init__.py'):
+ if os.path.isdir('plugins') and not os.path.exists('plugins/__init__.py'):
with open('plugins/__init__.py', 'w') as fh:
fh.write('# Plugin modules go here.')
@@ -175,7 +172,6 @@ def main(args=None):
class Help(DoitHelp):
-
"""Show Nikola usage."""
@staticmethod
@@ -199,7 +195,6 @@ class Help(DoitHelp):
class Build(DoitRun):
-
"""Expose "run" command as "build" for backwards compatibility."""
def __init__(self, *args, **kw):
@@ -234,20 +229,21 @@ class Build(DoitRun):
}
)
self.cmd_options = tuple(opts)
- super(Build, self).__init__(*args, **kw)
+ super().__init__(*args, **kw)
class Clean(DoitClean):
-
"""Clean site, including the cache directory."""
- def clean_tasks(self, tasks, dryrun):
+ # The unseemly *a is because this API changed between doit 0.30.1 and 0.31
+ def clean_tasks(self, tasks, dryrun, *a):
"""Clean tasks."""
if not dryrun and config:
cache_folder = config.get('CACHE_FOLDER', 'cache')
if os.path.exists(cache_folder):
shutil.rmtree(cache_folder)
- return super(Clean, self).clean_tasks(tasks, dryrun)
+ return super(Clean, self).clean_tasks(tasks, dryrun, *a)
+
# Nikola has its own "auto" commands that uses livereload.
# Expose original doit "auto" command as "doit_auto".
@@ -255,7 +251,6 @@ DoitAuto.name = 'doit_auto'
class NikolaTaskLoader(TaskLoader):
-
"""Nikola-specific task loader."""
def __init__(self, nikola, quiet=False):
@@ -277,18 +272,24 @@ class NikolaTaskLoader(TaskLoader):
}
DOIT_CONFIG['default_tasks'] = ['render_site', 'post_render']
DOIT_CONFIG.update(self.nikola._doit_config)
- tasks = generate_tasks(
- 'render_site',
- self.nikola.gen_tasks('render_site', "Task", 'Group of tasks to render the site.'))
- latetasks = generate_tasks(
- 'post_render',
- self.nikola.gen_tasks('post_render', "LateTask", 'Group of tasks to be executed after site is rendered.'))
- signal('initialized').send(self.nikola)
+ try:
+ tasks = generate_tasks(
+ 'render_site',
+ self.nikola.gen_tasks('render_site', "Task", 'Group of tasks to render the site.'))
+ latetasks = generate_tasks(
+ 'post_render',
+ self.nikola.gen_tasks('post_render', "LateTask", 'Group of tasks to be executed after site is rendered.'))
+ signal('initialized').send(self.nikola)
+ except Exception:
+ LOGGER.error('Error loading tasks. An unhandled exception occurred.')
+ if self.nikola.debug or self.nikola.show_tracebacks:
+ raise
+ _print_exception()
+ sys.exit(3)
return tasks + latetasks, DOIT_CONFIG
class DoitNikola(DoitMain):
-
"""Nikola-specific implementation of DoitMain."""
# overwite help command
@@ -297,7 +298,7 @@ class DoitNikola(DoitMain):
def __init__(self, nikola, quiet=False):
"""Initialzie DoitNikola."""
- super(DoitNikola, self).__init__()
+ super().__init__()
self.nikola = nikola
nikola.doit = self
self.task_loader = self.TASK_LOADER(nikola, quiet)
@@ -336,6 +337,8 @@ class DoitNikola(DoitMain):
if args[0] == 'help':
self.nikola.init_plugins(commands_only=True)
+ elif args[0] == 'plugin':
+ self.nikola.init_plugins(load_all=True)
else:
self.nikola.init_plugins()
@@ -359,12 +362,19 @@ class DoitNikola(DoitMain):
LOGGER.info('Did you mean "{}" or "{}"?'.format('", "'.join(best_sugg[:-1]), best_sugg[-1]))
return 3
- if sub_cmds[args[0]] is not Help and not isinstance(sub_cmds[args[0]], Command): # Is a doit command
+ if not sub_cmds[args[0]] in (Help, TabCompletion) and not isinstance(sub_cmds[args[0]], Command):
if not self.nikola.configured:
LOGGER.error("This command needs to run inside an "
"existing Nikola site.")
return 3
- return super(DoitNikola, self).run(cmd_args)
+ try:
+ return super().run(cmd_args)
+ except Exception:
+ LOGGER.error('An unhandled exception occurred.')
+ if self.nikola.debug or self.nikola.show_tracebacks:
+ raise
+ _print_exception()
+ return 1
@staticmethod
def print_version():
@@ -372,6 +382,53 @@ class DoitNikola(DoitMain):
print("Nikola v" + __version__)
+# Override Command.help() to make it more readable and to remove
+# some doit-specific stuff. Based on doit's implementation.
+# (see Issue #3342)
+def _command_help(self: Command):
+ """Return help text for a command."""
+ text = []
+
+ usage = "{} {} {}".format(self.bin_name, self.name, self.doc_usage)
+ text.extend(textwrap.wrap(usage, subsequent_indent=' '))
+ text.extend(_wrap(self.doc_purpose, 4))
+
+ text.append("\nOptions:")
+ options = defaultdict(list)
+ for opt in self.cmdparser.options:
+ options[opt.section].append(opt)
+ for section, opts in sorted(options.items()):
+ if section:
+ section_name = '\n{}'.format(section)
+ text.extend(_wrap(section_name, 2))
+ for opt in opts:
+ # ignore option that cant be modified on cmd line
+ if not (opt.short or opt.long):
+ continue
+ text.extend(_wrap(opt.help_param(), 4))
+ opt_help = opt.help
+ 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_choices = opt.help_choices()
+ desc = '{} {}'.format(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))
+
+ if self.doc_description is not None:
+ text.append("\n\nDescription:")
+ text.extend(_wrap(self.doc_description, 4))
+ return "\n".join(text)
+
+
+doit.cmd_base.Command.help = _command_help
+
+
def levenshtein(s1, s2):
u"""Calculate the Levenshtein distance of two strings.
@@ -399,5 +456,13 @@ def levenshtein(s1, s2):
return previous_row[-1]
+
+def _print_exception():
+ """Print an exception in a friendlier, shorter style."""
+ etype, evalue, _ = sys.exc_info()
+ LOGGER.error(''.join(traceback.format_exception(etype, evalue, None, limit=0, chain=False)).strip())
+ LOGGER.warning("To see more details, run Nikola in debug mode (set environment variable NIKOLA_DEBUG=1) or use NIKOLA_SHOW_TRACEBACKS=1")
+
+
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))