From 6d3f74142cf6cad046ce5a2842ce1a24814baf46 Mon Sep 17 00:00:00 2001 From: Tobias Gruetzmacher Date: Mon, 16 May 2016 14:57:47 +0200 Subject: [PATCH] Move command line tool into package. This way we can use the default Python console_scripts install process. --- dosage | 315 +-------------------------------------------- dosagelib/cmd.py | 329 +++++++++++++++++++++++++++++++++++++++++++++++ setup.cfg | 6 +- 3 files changed, 335 insertions(+), 315 deletions(-) create mode 100644 dosagelib/cmd.py diff --git a/dosage b/dosage index e00d5dac7..bf2023ed0 100755 --- a/dosage +++ b/dosage @@ -11,321 +11,10 @@ # |___/ from __future__ import absolute_import, division, print_function + import sys -import os -import argparse -import dosagelib -from dosagelib import events, configuration, singleton, director -from dosagelib.output import out -from dosagelib.util import internal_error, strlimit - - -class ArgumentParser(argparse.ArgumentParser): - """Custom argument parser.""" - - def print_help(self, file=None): - """Paginate help message on TTYs.""" - with out.pager(): - out.info(self.format_help()) - -Examples = """\ -EXAMPLES -List available comics: - dosage -l - -Get the latest comic of for example CalvinAndHobbes and save it in the "Comics" -directory: - dosage CalvinAndHobbes - -If you already have downloaded several comics and want to get the latest -strips of all of them: - dosage --continue @ -""" - - -def setup_options(): - """Construct option parser. - @return: new option parser - @rtype argparse.ArgumentParser - """ - kwargs = dict( - description="A comic downloader and archiver.", - epilog=Examples, - formatter_class=argparse.RawDescriptionHelpFormatter, - ) - parser = ArgumentParser(**kwargs) - parser.add_argument('-v', '--verbose', action='count', default=0, help='provides verbose output, use multiple times for more verbosity') - parser.add_argument('-n', '--numstrips', action='store', type=int, default=0, help='traverse and retrieve the given number of comic strips; use --all to retrieve all comic strips') - parser.add_argument('-a', '--all', action='store_true', help='traverse and retrieve all comic strips') - parser.add_argument('-c', '--continue', action='store_true', dest='cont', help='traverse and retrieve comic strips until an existing one is found') - parser.add_argument('-b', '--basepath', action='store', default='Comics', help='set the path to create invidivual comic directories in, default is Comics', metavar='PATH') - parser.add_argument('--baseurl', action='store', help='the base URL of your comics directory (for RSS, HTML, etc.); this should correspond to --base-path', metavar='PATH') - parser.add_argument('-l', '--list', action='store_true', help='list available comic modules') - parser.add_argument('--singlelist', action='store_true', help='list available comic modules in a single list') - parser.add_argument('--version', action='store_true', help='display the version number') - parser.add_argument('--vote', action='store_true', help='vote for the selected comics') - parser.add_argument('-m', '--modulehelp', action='store_true', help='display help for comic modules') - parser.add_argument('-t', '--timestamps', action='store_true', help='print timestamps for all output at any info level') - parser.add_argument('-o', '--output', action='append', dest='handler', choices=events.getHandlerNames(), help='sets output handlers for downloaded comics') - parser.add_argument('--no-downscale', action='store_false', dest='allowdownscale', help='prevent downscaling when using html or rss handler') - parser.add_argument('-p', '--parallel', action='store', type=int, default=1, help='fetch comics in parallel. Specify the number of connections') - parser.add_argument('--adult', action='store_true', help='confirms that you are old enough to view adult content') - parser.add_argument('--allow-multiple', action='store_true', help='allows multiple instances to run at the same time. Use if you know what you are doing.') - # used for development testing prev/next matching - parser.add_argument('--dry-run', action='store_true', help=argparse.SUPPRESS) - # multimatch is only used for development, eg. testing if all comics of a scripted plugin are working - parser.add_argument('--multimatch', action='store_true', help=argparse.SUPPRESS) - parser.add_argument('comic', nargs='*', help='comic module name (including case insensitive substrings)') - try: - import argcomplete - argcomplete.autocomplete(parser) - except ImportError: - pass - return parser - - -def display_version(verbose): - """Display application name, version, copyright and license.""" - print(configuration.App) - print(configuration.Copyright) - print(configuration.Freeware) - print("For support see", configuration.SupportUrl) - if verbose: - # search for updates - from dosagelib.updater import check_update - result, value = check_update() - if result: - if value: - version, url = value - if url is None: - # current version is newer than online version - text = ('Detected local or development version %(currentversion)s. ' - 'Available version of %(app)s is %(version)s.') - else: - # display update link - text = ('A new version %(version)s of %(app)s is ' - 'available at %(url)s.') - attrs = dict(version=version, app=configuration.AppName, - url=url, currentversion=dosagelib.__version__) - print(text % attrs) - else: - if value is None: - value = 'invalid update file syntax' - text = ('An error occured while checking for an ' - 'update of %(app)s: %(error)s.') - attrs = dict(error=value, app=configuration.AppName) - print(text % attrs) - return 0 - - -def set_output_info(options): - """Set global output level and timestamp option.""" - out.level = 0 - out.level += options.verbose - out.timestamps = options.timestamps - - -def display_help(options): - """Print help for comic strips.""" - errors = 0 - try: - for scraperobj in director.getScrapers(options.comic, options.basepath, listing=True): - errors += display_comic_help(scraperobj) - except ValueError as msg: - out.exception(msg) - return 2 - return errors - - -def display_comic_help(scraperobj): - """Print help for a comic.""" - orig_context = out.context - out.context = scraperobj.name - try: - out.info(u"URL: " + scraperobj.url) - out.info(u"Language: " + scraperobj.language()) - if scraperobj.adult: - out.info(u"Adult comic, use option --adult to fetch.") - disabled = scraperobj.getDisabledReasons() - if disabled: - out.info(u"Disabled: " + " ".join(disabled.values())) - if scraperobj.help: - for line in scraperobj.help.splitlines(): - out.info(line) - return 0 - except ValueError as msg: - out.exception(msg) - return 1 - finally: - out.context = orig_context - - -def vote_comics(options): - """Vote for comics.""" - errors = 0 - try: - for scraperobj in director.getScrapers(options.comic, options.basepath, - options.adult, - options.multimatch): - errors += vote_comic(scraperobj) - except ValueError as msg: - out.exception(msg) - errors += 1 - return errors - - -def vote_comic(scraperobj): - """Vote for given comic scraper.""" - errors = 0 - orig_context = out.context - out.context = scraperobj.name - try: - name = scraperobj.name - answer = scraperobj.vote() - out.debug(u'Vote answer %r' % answer) - if answer == 'counted': - url = configuration.Url + 'comics/%s.html' % name.replace('/', '_') - out.info(u'Vote submitted. Votes are updated regularly at %s.' % url) - elif answer == 'no': - out.info(u'Vote not submitted - your vote has already been submitted before.') - elif answer == 'noname': - out.warn(u'The comic %s cannot be voted.' % name) - else: - out.warn(u'Error submitting vote parameters: %r' % answer) - except Exception as msg: - out.exception(msg) - errors += 1 - finally: - out.context = orig_context - return errors - - -def run(options): - """Execute comic commands.""" - set_output_info(options) - # ensure only one instance of dosage is running - if not options.allow_multiple: - singleton.SingleInstance() - if options.version: - return display_version(options.verbose) - if options.list: - return do_list() - if options.singlelist: - return do_list(column_list=False, verbose=options.verbose) - # after this a list of comic strips is needed - if not options.comic: - out.warn(u'No comics specified, bailing out!') - return 1 - if options.modulehelp: - return display_help(options) - if options.vote: - return vote_comics(options) - return director.getComics(options) - - -def do_list(column_list=True, verbose=False): - """List available comics.""" - with out.pager(): - out.info(u'Available comic scrapers:') - out.info(u'Comics tagged with [%s] require age confirmation with the --adult option.' % TAG_ADULT) - out.info(u'Non-english comics are tagged with [%s].' % TAG_LANG) - scrapers = sorted(director.getAllScrapers(listing=True), - key=lambda s: s.name) - if column_list: - num, disabled = do_column_list(scrapers) - else: - num, disabled = do_single_list(scrapers, verbose=verbose) - out.info(u'%d supported comics.' % num) - if disabled: - out.info('') - out.info(u'Some comics are disabled, they are tagged with [%s:REASON], where REASON is one of:' % TAG_DISABLED) - for k in disabled: - out.info(u' %-10s %s' % (k, disabled[k])) - return 0 - - -def do_single_list(scrapers, verbose=False): - """Get list of scraper names, one per line.""" - disabled = {} - for num, scraperobj in enumerate(scrapers): - if verbose: - display_comic_help(scraperobj) - else: - out.info(get_tagged_scraper_name(scraperobj, reasons=disabled)) - return num, disabled - - -def do_column_list(scrapers): - """Get list of scraper names with multiple names per line.""" - disabled = {} - width = out.width - # limit name length so at least two columns are there - limit = (width // 2) - 8 - names = [get_tagged_scraper_name(scraperobj, limit=limit, reasons=disabled) - for scraperobj in scrapers] - num = len(names) - maxlen = max(len(name) for name in names) - names_per_line = max(width // (maxlen + 1), 1) - while names: - out.info(u''.join(name.ljust(maxlen) for name in - names[:names_per_line])) - del names[:names_per_line] - return num, disabled - -TAG_ADULT = "adult" -TAG_LANG = "lang" -TAG_DISABLED = "dis" - - -def get_tagged_scraper_name(scraperobj, limit=None, reasons=None): - """Get comic scraper name.""" - tags = [] - if scraperobj.adult: - tags.append(TAG_ADULT) - if scraperobj.lang != "en": - tags.append("%s:%s" % (TAG_LANG, scraperobj.lang)) - disabled = scraperobj.getDisabledReasons() - if disabled and reasons is not None: - reasons.update(disabled) - for reason in disabled: - tags.append("%s:%s" % (TAG_DISABLED, reason)) - if tags: - suffix = " [" + ", ".join(tags) + "]" - else: - suffix = "" - name = scraperobj.name - if limit is not None: - name = strlimit(name, limit) - return name + suffix - - -def main(): - """Parse options and execute commands.""" - try: - options = setup_options().parse_args() - options.basepath = os.path.expanduser(options.basepath) - res = run(options) - except KeyboardInterrupt: - print("Aborted.") - res = 1 - except Exception: - internal_error() - res = 2 - return res - - -def profile(): - """Profile the loading of all scrapers.""" - import cProfile - cProfile.run("scraper.get_scrapers()", "dosage.prof") - - -def viewprof(): - """View profile stats.""" - import pstats - stats = pstats.Stats("dosage.prof") - stats.strip_dirs().sort_stats("cumulative").print_stats(100) +from dosagelib.cmd import main if __name__ == '__main__': sys.exit(main()) diff --git a/dosagelib/cmd.py b/dosagelib/cmd.py new file mode 100644 index 000000000..a91ed8e5d --- /dev/null +++ b/dosagelib/cmd.py @@ -0,0 +1,329 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2004-2005 Tristan Seligmann and Jonathan Jacobs +# Copyright (C) 2012-2014 Bastian Kleineidam +# Copyright (C) 2015-2016 Tobias Gruetzmacher + +from __future__ import absolute_import, division, print_function + +import os +import argparse + +from . import events, configuration, singleton, director, __version__ +from .output import out +from .util import internal_error, strlimit + + +class ArgumentParser(argparse.ArgumentParser): + """Custom argument parser.""" + + def print_help(self, file=None): + """Paginate help message on TTYs.""" + with out.pager(): + out.info(self.format_help()) + +Examples = """\ +EXAMPLES +List available comics: + dosage -l + +Get the latest comic of for example CalvinAndHobbes and save it in the "Comics" +directory: + dosage CalvinAndHobbes + +If you already have downloaded several comics and want to get the latest +strips of all of them: + dosage --continue @ +""" + + +def setup_options(): + """Construct option parser. + @return: new option parser + @rtype argparse.ArgumentParser + """ + parser = ArgumentParser( + description="A comic downloader and archiver.", + epilog=Examples, + formatter_class=argparse.RawDescriptionHelpFormatter) + + parser.add_argument('-v', '--verbose', action='count', default=0, + help='provides verbose output, use multiple times for more verbosity') + parser.add_argument('-n', '--numstrips', action='store', type=int, default=0, + help='traverse and retrieve the given number of comic strips; use --all to retrieve all comic strips') + parser.add_argument('-a', '--all', action='store_true', + help='traverse and retrieve all comic strips') + parser.add_argument('-c', '--continue', action='store_true', dest='cont', + help='traverse and retrieve comic strips until an existing one is found') + parser.add_argument('-b', '--basepath', action='store', default='Comics', + metavar='PATH', + help='set the path to create invidivual comic directories in, default is Comics') + parser.add_argument('--baseurl', action='store', metavar='PATH', + help='the base URL of your comics directory (for RSS, HTML, etc.); this should correspond to --base-path') + parser.add_argument('-l', '--list', action='store_true', + help='list available comic modules') + parser.add_argument('--singlelist', action='store_true', + help='list available comic modules in a single column list') + parser.add_argument('--version', action='store_true', + help='display the version number') + parser.add_argument('--vote', action='store_true', + help='vote for the selected comics') + parser.add_argument('-m', '--modulehelp', action='store_true', + help='display help for comic modules') + parser.add_argument('-t', '--timestamps', action='store_true', + help='print timestamps for all output at any info level') + parser.add_argument('-o', '--output', action='append', dest='handler', + choices=events.getHandlerNames(), + help='sets output handlers for downloaded comics') + parser.add_argument('--no-downscale', action='store_false', + dest='allowdownscale', + help='prevent downscaling when using html or rss handler') + parser.add_argument('-p', '--parallel', action='store', type=int, default=1, + help='fetch comics in parallel. Specify the number of connections') + parser.add_argument('--adult', action='store_true', + help='confirms that you are old enough to view adult content') + parser.add_argument('--allow-multiple', action='store_true', + help='allows multiple instances to run at the same time. Use if you know what you are doing.') + # used for development testing prev/next matching + parser.add_argument('--dry-run', action='store_true', + help=argparse.SUPPRESS) + # multimatch is only used for development, eg. testing if all comics of a scripted plugin are working + parser.add_argument('--multimatch', action='store_true', + help=argparse.SUPPRESS) + parser.add_argument('comic', nargs='*', + help='comic module name (including case insensitive substrings)') + try: + import argcomplete + argcomplete.autocomplete(parser) + except ImportError: + pass + return parser + + +def display_version(verbose): + """Display application name, version, copyright and license.""" + print(configuration.App) + print(configuration.Copyright) + print(configuration.Freeware) + print("For support see", configuration.SupportUrl) + if verbose: + # search for updates + from .updater import check_update + result, value = check_update() + if result: + if value: + version, url = value + if url is None: + # current version is newer than online version + text = ('Detected local or development version %(currentversion)s. ' + 'Available version of %(app)s is %(version)s.') + else: + # display update link + text = ('A new version %(version)s of %(app)s is ' + 'available at %(url)s.') + attrs = dict(version=version, app=configuration.AppName, + url=url, currentversion=__version__) + print(text % attrs) + else: + if value is None: + value = 'invalid update file syntax' + text = ('An error occured while checking for an ' + 'update of %(app)s: %(error)s.') + attrs = dict(error=value, app=configuration.AppName) + print(text % attrs) + return 0 + + +def set_output_info(options): + """Set global output level and timestamp option.""" + out.level = 0 + out.level += options.verbose + out.timestamps = options.timestamps + + +def display_help(options): + """Print help for comic strips.""" + errors = 0 + try: + for scraperobj in director.getScrapers(options.comic, options.basepath, listing=True): + errors += display_comic_help(scraperobj) + except ValueError as msg: + out.exception(msg) + return 2 + return errors + + +def display_comic_help(scraperobj): + """Print help for a comic.""" + orig_context = out.context + out.context = scraperobj.name + try: + out.info(u"URL: " + scraperobj.url) + out.info(u"Language: " + scraperobj.language()) + if scraperobj.adult: + out.info(u"Adult comic, use option --adult to fetch.") + disabled = scraperobj.getDisabledReasons() + if disabled: + out.info(u"Disabled: " + " ".join(disabled.values())) + if scraperobj.help: + for line in scraperobj.help.splitlines(): + out.info(line) + return 0 + except ValueError as msg: + out.exception(msg) + return 1 + finally: + out.context = orig_context + + +def vote_comics(options): + """Vote for comics.""" + errors = 0 + try: + for scraperobj in director.getScrapers(options.comic, options.basepath, + options.adult, + options.multimatch): + errors += vote_comic(scraperobj) + except ValueError as msg: + out.exception(msg) + errors += 1 + return errors + + +def vote_comic(scraperobj): + """Vote for given comic scraper.""" + errors = 0 + orig_context = out.context + out.context = scraperobj.name + try: + name = scraperobj.name + answer = scraperobj.vote() + out.debug(u'Vote answer %r' % answer) + if answer == 'counted': + url = configuration.Url + 'comics/%s.html' % name.replace('/', '_') + out.info(u'Vote submitted. Votes are updated regularly at %s.' % url) + elif answer == 'no': + out.info(u'Vote not submitted - your vote has already been submitted before.') + elif answer == 'noname': + out.warn(u'The comic %s cannot be voted.' % name) + else: + out.warn(u'Error submitting vote parameters: %r' % answer) + except Exception as msg: + out.exception(msg) + errors += 1 + finally: + out.context = orig_context + return errors + + +def run(options): + """Execute comic commands.""" + set_output_info(options) + # ensure only one instance of dosage is running + if not options.allow_multiple: + singleton.SingleInstance() + if options.version: + return display_version(options.verbose) + if options.list: + return do_list() + if options.singlelist: + return do_list(column_list=False, verbose=options.verbose) + # after this a list of comic strips is needed + if not options.comic: + out.warn(u'No comics specified, bailing out!') + return 1 + if options.modulehelp: + return display_help(options) + if options.vote: + return vote_comics(options) + return director.getComics(options) + + +def do_list(column_list=True, verbose=False): + """List available comics.""" + with out.pager(): + out.info(u'Available comic scrapers:') + out.info(u'Comics tagged with [%s] require age confirmation with the --adult option.' % TAG_ADULT) + out.info(u'Non-english comics are tagged with [%s].' % TAG_LANG) + scrapers = sorted(director.getAllScrapers(listing=True), + key=lambda s: s.name) + if column_list: + num, disabled = do_column_list(scrapers) + else: + num, disabled = do_single_list(scrapers, verbose=verbose) + out.info(u'%d supported comics.' % num) + if disabled: + out.info('') + out.info(u'Some comics are disabled, they are tagged with [%s:REASON], where REASON is one of:' % TAG_DISABLED) + for k in disabled: + out.info(u' %-10s %s' % (k, disabled[k])) + return 0 + + +def do_single_list(scrapers, verbose=False): + """Get list of scraper names, one per line.""" + disabled = {} + for num, scraperobj in enumerate(scrapers): + if verbose: + display_comic_help(scraperobj) + else: + out.info(get_tagged_scraper_name(scraperobj, reasons=disabled)) + return num, disabled + + +def do_column_list(scrapers): + """Get list of scraper names with multiple names per line.""" + disabled = {} + width = out.width + # limit name length so at least two columns are there + limit = (width // 2) - 8 + names = [get_tagged_scraper_name(scraperobj, limit=limit, reasons=disabled) + for scraperobj in scrapers] + num = len(names) + maxlen = max(len(name) for name in names) + names_per_line = max(width // (maxlen + 1), 1) + while names: + out.info(u''.join(name.ljust(maxlen) for name in + names[:names_per_line])) + del names[:names_per_line] + return num, disabled + +TAG_ADULT = "adult" +TAG_LANG = "lang" +TAG_DISABLED = "dis" + + +def get_tagged_scraper_name(scraperobj, limit=None, reasons=None): + """Get comic scraper name.""" + tags = [] + if scraperobj.adult: + tags.append(TAG_ADULT) + if scraperobj.lang != "en": + tags.append("%s:%s" % (TAG_LANG, scraperobj.lang)) + disabled = scraperobj.getDisabledReasons() + if disabled and reasons is not None: + reasons.update(disabled) + for reason in disabled: + tags.append("%s:%s" % (TAG_DISABLED, reason)) + if tags: + suffix = " [" + ", ".join(tags) + "]" + else: + suffix = "" + name = scraperobj.name + if limit is not None: + name = strlimit(name, limit) + return name + suffix + + +def main(): + """Parse options and execute commands.""" + try: + options = setup_options().parse_args() + options.basepath = os.path.expanduser(options.basepath) + res = run(options) + except KeyboardInterrupt: + print("Aborted.") + res = 1 + except Exception: + internal_error() + res = 2 + return res diff --git a/setup.cfg b/setup.cfg index 4540553ae..f9c7de28d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,8 +27,10 @@ classifier = [files] packages = dosagelib -data_files = - bin = dosage + +[entry_points] +console_scripts = + dosage = dosagelib.cmd:main [extras] css =