diff options
Diffstat (limited to 'nikola/__main__.py')
| -rw-r--r-- | nikola/__main__.py | 176 |
1 files changed, 121 insertions, 55 deletions
diff --git a/nikola/__main__.py b/nikola/__main__.py index f002768..8330e67 100644 --- a/nikola/__main__.py +++ b/nikola/__main__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2016 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,38 +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 doit.cmd_help import Help as DoitHelp -from doit.cmd_run import Run as DoitRun +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_auto import Auto as DoitAuto -from logbook import NullHandler -from blinker import signal +from doit.cmd_help import Help as DoitHelp +from doit.cmd_run import Run as DoitRun +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 = {} @@ -68,10 +67,10 @@ _RETURN_DOITNIKOLA = False def main(args=None): """Run Nikola.""" colorful = False - if sys.stderr.isatty() and os.name != 'nt' and os.getenv('NIKOLA_MONO') is None: + 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:] @@ -80,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 @@ -119,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: @@ -155,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.') @@ -233,19 +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". @@ -274,13 +272,20 @@ 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 @@ -293,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) @@ -362,7 +367,14 @@ class DoitNikola(DoitMain): 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(): @@ -370,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. @@ -398,5 +457,12 @@ 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:])) |
