# -*- coding: utf-8 -*-
# Copyright (C) 2014 Johannes Baiter <johannes.baiter@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Core logic for application startup and parsing of command-line arguments
"""
from __future__ import division, unicode_literals, print_function
import argparse
import logging
import logging.handlers
import os
import sys
import traceback
import colorama
from spreads.vendor.confit import ConfigError
from pathlib import Path
import spreads.cli as cli
import spreads.plugin as plugin
import spreads.util as util
from spreads.config import Configuration
[docs]def add_argument_from_template(extname, key, template, parser, current_val):
""" Add option from `template` to `parser` under the name `key`.
Templates with a boolean value type will create a `--<key>` or
`--no-<key>` flag, depending on their current value.
:param extname: Name of the configuration section this option's result
should be stored in
:param key: Configuration key in section, will also determine the
name of the argument.
:param template: Template for the argument
:type template: :py:class:`spreads.config.OptionTemplate`
:param parser: Argument parser the argument should be added to
:type parser: :py:class:`argparse.ArgumentParser`
:param current_val: Current value of the option
"""
flag = "--{0}".format(key.replace('_', '-'))
default = current_val
kwargs = {'help': ("{0} [default: {1}]"
.format(template.docstring, default)),
'dest': "{0}{1}".format(extname, '.'+key if extname else key)}
if isinstance(template.value, basestring) or template.value is None:
kwargs['type'] = unicode
kwargs['metavar'] = "<str>"
elif isinstance(template.value, bool):
kwargs['help'] = template.docstring
if current_val:
flag = "--no-{0}".format(key.replace('_', '-'))
kwargs['help'] = ("Disable {0}"
.format(template.docstring.lower()))
kwargs['action'] = "store_false"
else:
kwargs['action'] = "store_true"
elif isinstance(template.value, float):
kwargs['type'] = float
kwargs['metavar'] = "<float>"
elif isinstance(template.value, int):
kwargs['type'] = int
kwargs['metavar'] = "<int>"
elif template.selectable:
kwargs['type'] = type(template.value[0])
kwargs['metavar'] = "<{0}>".format("/".join(template.value))
kwargs['choices'] = template.value
else:
raise TypeError("Unsupported option type")
parser.add_argument(flag, **kwargs)
[docs]def should_show_argument(template, active_plugins):
""" Checks the :py:attr:`spreads.config.OptionTemplate.depends` attribute
for dependencies on other plugins and validates them against the list of
activated plugins.
We do not validate dependencies on other configuration settings because
we don't have access to the final state of the configuration at this time,
since the configuration can potentially be changed by other command-line
flags.
:param template: Template to check
:type template: :py:class:`spreads.config.OptionTemplate`
:param active_plugins: List of names of activated plugins
:returns: Whether or not the argument should be displayed
"""
if template.depends is None or type(template.depends) == dict:
return True
else:
return template.depends in active_plugins
[docs]def setup_parser(config):
""" Sets up an :py:class:`argparse.ArgumentParser` instance with all
options and subcommands that are available in the core and activated
plugins.
:param config: Current application configuration
:type config: :py:class:`spreads.config.Configuration`
:returns: Fully initialized argument parser
:rtype: :py:class:`argparse.ArgumentParser`
"""
plugins = plugin.get_plugins(*config["plugins"].get())
def _add_arguments(parsers, mixins, extra_names=None):
if extra_names is None:
extra_names = []
for parser in parsers:
# Only plugins that implement the capture or trigger hook mixins
# and the currently active device configuration are relevant for
# this subcommand.
ext_names = [name for name, cls in plugins.iteritems()
if any(issubclass(cls, mixin) for mixin in mixins)]
ext_names.extend(extra_names)
for ext in ext_names:
for key, tmpl in config.templates.get(ext, {}).iteritems():
if not should_show_argument(option,
config['plugins'].get()):
continue
try:
add_argument_from_template(ext, key, tmpl, parser,
config[ext][key].get())
except TypeError:
continue
rootparser = argparse.ArgumentParser(
description="Scanning Tool for DIY Book Scanner",
formatter_class=argparse.RawDescriptionHelpFormatter)
rootparser.add_argument(
'-V', '--version', action='version',
version=(
"spreads {0}\n\n"
"Licensed under the terms of the GNU Affero General Public "
"License 3.0.\n"
"(C) 2013-2014 Johannes Baiter <johannes.baiter@gmail.com>\n"
"For a complete list of contributors see:\n"
"https://github.com/DIYBookScanner/spreads/graphs/contributors\n\n"
.format(util.get_version())))
for key, option in config.templates['core'].iteritems():
if not should_show_argument(option, config['plugins'].get()):
continue
try:
add_argument_from_template('core', key, option, rootparser,
config['core'][key].get())
except TypeError:
continue
subparsers = rootparser.add_subparsers()
wizard_parser = subparsers.add_parser(
'wizard', help="Interactive mode")
wizard_parser.add_argument(
"path", type=unicode, help="Project path")
wizard_parser.set_defaults(subcommand=cli.wizard)
config_parser = subparsers.add_parser(
'configure', help="Perform initial configuration")
config_parser.set_defaults(subcommand=cli.configure)
try:
import spreads.tkconfigure as tkconfigure
guiconfig_parser = subparsers.add_parser(
'guiconfigure', help="Perform initial configuration with a GUI")
guiconfig_parser.set_defaults(subcommand=tkconfigure.configure)
except ImportError:
pass
capture_parser = subparsers.add_parser(
'capture', help="Start the capturing workflow")
capture_parser.add_argument(
"path", type=unicode, help="Project path")
capture_parser.set_defaults(subcommand=cli.capture)
# Add arguments from plugins
_add_arguments(parsers=(capture_parser, wizard_parser),
mixins=(plugin.CaptureHooksMixin, plugin.TriggerHooksMixin),
extra_names=('device',))
postprocess_parser = subparsers.add_parser(
'postprocess',
help="Postprocess scanned images.")
postprocess_parser.add_argument(
"path", type=unicode, help="Project path")
postprocess_parser.add_argument(
"--jobs", "-j", dest="jobs", type=int, default=None,
metavar="<int>", help="Number of concurrent processes")
postprocess_parser.set_defaults(subcommand=cli.postprocess)
_add_arguments(parsers=(postprocess_parser, wizard_parser),
mixins=(plugin.ProcessHooksMixin,))
output_parser = subparsers.add_parser(
'output',
help="Generate output files.")
output_parser.add_argument(
"path", type=unicode, help="Project path")
output_parser.set_defaults(subcommand=cli.output)
_add_arguments(parsers=(output_parser, wizard_parser),
mixins=(plugin.OutputHooksMixin,))
# Add custom subcommands from plugins
if config["plugins"].get():
classes = (cls for cls in plugins.values()
if issubclass(cls, plugin.SubcommandHooksMixin))
for cls in classes:
cls.add_command_parser(subparsers, config)
return rootparser
[docs]def setup_logging(config):
""" Conigure application-wide logger.
:param config: Global configuration
:type config: :py:class:`spreads.config.Configuration`
"""
loglevel = config['core']['loglevel'].as_choice({
'none': logging.NOTSET,
'info': logging.INFO,
'debug': logging.DEBUG,
'warning': logging.WARNING,
'error': logging.ERROR,
'critical': logging.CRITICAL,
})
logger = logging.getLogger()
# Remove previous handlers
if logger.handlers:
for handler in logger.handlers:
logger.removeHandler(handler)
# Add stderr handler
if util.is_os('windows'):
stdout_handler = logging.StreamHandler()
else:
stdout_handler = util.ColourStreamHandler()
stdout_handler.setLevel(logging.DEBUG if config['core']['verbose'].get()
else logging.WARNING)
stdout_handler.setFormatter(logging.Formatter("%(name)s: %(message)s"))
logger.addHandler(stdout_handler)
# Add event handler
logger.addHandler(util.EventHandler())
# Add logfile handler
logfile = Path(config['core']['logfile'].as_filename())
if not logfile.parent.exists():
logfile.parent.mkdir()
file_handler = logging.handlers.RotatingFileHandler(
filename=unicode(logfile), maxBytes=512*1024, backupCount=1)
file_handler.setFormatter(logging.Formatter(
'%(relativeCreated)s %(name)-5s %(levelname)-8s %(message)s'))
file_handler.setLevel(loglevel)
logger.addHandler(file_handler)
# Set root logger level (needed for web plugin)
logger.setLevel(logging.DEBUG)
[docs]def run_config_windows():
""" Entry point to launch graphical configuration dialog on Windows. """
# Needed so that .exe files in Program Files can be launched.
os.environ['PATH'] += (";" + os.environ['PROGRAMFILES'])
config = Configuration()
setup_logging(config)
from spreads.tkconfigure import configure
configure(config)
[docs]def run_service_windows():
""" Entry point to launch web plugin server on Windows. """
# Needed so that .exe files in Program Files can be launched.
os.environ['PATH'] += (";" + os.environ['PROGRAMFILES'])
config = Configuration()
config['core']['loglevel'] = 'debug'
if not config['plugins'].get():
config['plugins'] = ['autorotate', 'scantailor', 'tesseract',
'pdfbeads', 'web']
config.load_defaults(overwrite=False)
setup_logging(config)
from spreadsplug.web import run_windows_service
config['web']['mode'] = 'processor'
run_windows_service(config)
[docs]def run():
""" Setup the application and run subcommand"""
config = Configuration()
parser = setup_parser(config)
args = parser.parse_args()
config.set_from_args(args)
setup_logging(config)
args.subcommand(config)
[docs]def main():
""" Entry point for `spread` command-line application. """
# Initialize color support
colorama.init()
def print_error(x):
print(util.colorize(x, colorama.Fore.RED),
file=sys.stderr)
try:
run()
except util.DeviceException as e:
typ, val, tb = sys.exc_info()
logging.debug("".join(traceback.format_exception(typ, val, tb)))
print_error("There is a problem with your device configuration:")
print_error(e.message)
except ConfigError as e:
typ, val, tb = sys.exc_info()
logging.debug("".join(traceback.format_exception(typ, val, tb)))
print_error("There is a problem with your configuration file(s):")
print_error(e.message)
except util.MissingDependencyException as e:
typ, val, tb = sys.exc_info()
logging.debug("".join(traceback.format_exception(typ, val, tb)))
print_error("You are missing a dependency for one of your "
"enabled plugins:")
print_error(e.message)
except KeyboardInterrupt:
colorama.deinit()
sys.exit(1)
except Exception as e:
typ, val, tb = sys.exc_info()
print_error("spreads encountered an error:")
print_error("".join(traceback.format_exception(typ, val, tb)))
# Deinitialize color support
colorama.deinit()
if __name__ == '__main__':
logging.basicConfig(loglevel=logging.ERROR)
main()