diff --git a/patacrep/build.py b/patacrep/build.py index 2fed6c31..bf1e2918 100644 --- a/patacrep/build.py +++ b/patacrep/build.py @@ -172,11 +172,11 @@ class SongbookBuilder: # are function; values are return values of functions. _called_functions = {} - def __init__(self, raw_songbook, basename): - # Representation of the .sb songbook configuration file. - self.songbook = Songbook(raw_songbook, basename) + def __init__(self, raw_songbook): # Basename of the songbook to be built. - self.basename = basename + self.basename = raw_songbook['_basename'] + # Representation of the .sb songbook configuration file. + self.songbook = Songbook(raw_songbook, self.basename) def _run_once(self, function, *args, **kwargs): """Run function if it has not been run yet. diff --git a/patacrep/errors.py b/patacrep/errors.py index 08eebc6a..c111ca3b 100644 --- a/patacrep/errors.py +++ b/patacrep/errors.py @@ -7,7 +7,7 @@ class SongbookError(Exception): """ pass -class SBFileError(SongbookError): +class YAMLError(SongbookError): """Error during songbook file decoding""" def __init__(self, message=None): diff --git a/patacrep/songbook/__init__.py b/patacrep/songbook/__init__.py index e69de29b..20fd8da0 100644 --- a/patacrep/songbook/__init__.py +++ b/patacrep/songbook/__init__.py @@ -0,0 +1,54 @@ +"""Raw songbook utilities""" + +import logging +import os +import sys +import yaml + +from patacrep import encoding +import patacrep + +LOGGER = logging.getLogger() + +def open_songbook(filename): + """Open songbook, and return a raw songbook object. + + :param str filename: Filename of the yaml songbook. + :rvalue: dict + :return: Songbook, as a dictionary. + """ + if os.path.exists(filename + ".sb") and not os.path.exists(filename): + filename += ".sb" + + try: + with patacrep.encoding.open_read(filename) as songbook_file: + songbook = yaml.load(songbook_file) + if 'encoding' in songbook: + with encoding.open_read( + filename, + encoding=songbook['encoding'] + ) as songbook_file: + songbook = yaml.load(songbook_file) + except Exception as error: # pylint: disable=broad-except + raise patacrep.errors.YAMLError(str(error)) + + songbook['_basename'] = os.path.basename(filename)[:-3] + + # Gathering datadirs + datadirs = [] + if 'datadir' in songbook: + if isinstance(songbook['datadir'], str): + songbook['datadir'] = [songbook['datadir']] + datadirs += [ + os.path.join( + os.path.dirname(os.path.abspath(filename)), + path + ) + for path in songbook['datadir'] + ] + # Default value + datadirs.append(os.path.dirname(os.path.abspath(filename))) + + songbook['datadir'] = datadirs + + return songbook diff --git a/patacrep/songbook/__main__.py b/patacrep/songbook/__main__.py index b62dcf73..ed2364ff 100644 --- a/patacrep/songbook/__main__.py +++ b/patacrep/songbook/__main__.py @@ -3,20 +3,18 @@ import argparse import locale import logging -import os.path -import textwrap import sys -import yaml +import textwrap -from patacrep.build import SongbookBuilder, DEFAULT_STEPS -from patacrep.utils import yesno from patacrep import __version__ from patacrep import errors +from patacrep.build import SongbookBuilder, DEFAULT_STEPS +from patacrep.utils import yesno import patacrep.encoding # Logging configuration logging.basicConfig(level=logging.INFO) -LOGGER = logging.getLogger() +LOGGER = logging.getLogger("patatools") # pylint: disable=too-few-public-methods class ParseStepsAction(argparse.Action): @@ -127,49 +125,16 @@ def main(): options = argument_parser(sys.argv[1:]) - songbook_path = options.book[-1] - if os.path.exists(songbook_path + ".sb") and not os.path.exists(songbook_path): - songbook_path += ".sb" - - basename = os.path.basename(songbook_path)[:-3] - try: - with patacrep.encoding.open_read(songbook_path) as songbook_file: - songbook = yaml.load(songbook_file) - if 'encoding' in songbook: - with patacrep.encoding.open_read( - songbook_path, - encoding=songbook['encoding'] - ) as songbook_file: - songbook = yaml.load(songbook_file) - except Exception as error: # pylint: disable=broad-except - LOGGER.error(error) - LOGGER.error("Error while loading file '{}'.".format(songbook_path)) - sys.exit(1) + songbook = patacrep.songbook.open_songbook(options.book[-1]) - # Gathering datadirs - datadirs = [] - if options.datadir: # Command line options - datadirs += [item[0] for item in options.datadir] - if 'datadir' in songbook: - if isinstance(songbook['datadir'], str): - songbook['datadir'] = [songbook['datadir']] - datadirs += [ - os.path.join( - os.path.dirname(os.path.abspath(songbook_path)), - path - ) - for path in songbook['datadir'] - ] - # Default value - datadirs.append(os.path.dirname(os.path.abspath(songbook_path))) - - songbook['datadir'] = datadirs - songbook['_cache'] = options.cache[0] + if options.datadir: + for datadir in reversed(options.datadir): + songbook['datadir'].insert(0, datadir) + songbook['_cache'] = options.cache[0] - try: - sb_builder = SongbookBuilder(songbook, basename) + sb_builder = SongbookBuilder(songbook) sb_builder.unsafe = True sb_builder.build_steps(options.steps) diff --git a/patacrep/tools/__init__.py b/patacrep/tools/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/patacrep/tools/__main__.py b/patacrep/tools/__main__.py new file mode 100644 index 00000000..17f3c8a4 --- /dev/null +++ b/patacrep/tools/__main__.py @@ -0,0 +1,116 @@ +#!/bin/env python3 + +"""Command line client to :mod:`tools`""" + +import argparse +import logging +import operator +import os +import pkgutil +import re +import sys + +import patacrep + +# Logging configuration +logging.basicConfig(level=logging.INFO) +LOGGER = logging.getLogger() + +def _execlp(program, args): + """Call :func:`os.execlp`, adding `program` as the first argument to itself.""" + return os.execlp(program, program, *args) + +def _iter_subcommands(): + """Iterate over subcommands. + + The objects returned are tuples of: + - the name of the command; + - its description; + - the function to call to execute the subcommand. + """ + subcommands = [] + + # Get python subcommands + path = [os.path.join(item, "patacrep", "tools") for item in sys.path] + prefix = "patacrep.tools." + module_re = re.compile(r'{}(?P[^\.]*)\.__main__'.format(prefix)) + for module_loader, name, _ in pkgutil.walk_packages(path, prefix): + match = module_re.match(name) + if match: + module = module_loader.find_module(match.string).load_module() + if hasattr(module, "SUBCOMMAND_DESCRIPTION"): + subcommands.append(match.groupdict()['subcommand']) + yield ( + match.groupdict()['subcommand'], + getattr(module, "SUBCOMMAND_DESCRIPTION"), + module.main, + ) + +class ArgumentParser(argparse.ArgumentParser): + """Proxy class to circumvent an :mod:`argparse` bug. + + Contrarily to what documented, the `argparse.REMAINDER + `_ `nargs` setting + does not include the remainder arguments if the first one begins with `-`. + + This bug is reperted as `17050 `_. This + class can be deleted once this bug has been fixed. + """ + + def parse_args(self, args=None, namespace=None): + if args is None: + args = sys.argv[1:] + subcommands = [command[0] for command in set(_iter_subcommands())] + if len(args) > 0: + if args[0] in subcommands: + args = [args[0], "--"] + args[1:] + + value = super().parse_args(args, namespace) + + if hasattr(value, 'remainder'): + value.remainder = value.remainder[1:] + return value + + +def commandline_parser(): + """Return a command line parser.""" + + parser = ArgumentParser( + prog="patatools", + description=( + "Miscellaneous tools for patacrep." + ), + formatter_class=argparse.RawTextHelpFormatter, + ) + + parser.add_argument( + '--version', + help='Show version', + action='version', + version='%(prog)s ' + patacrep.__version__ + ) + + subparsers = parser.add_subparsers( + title="Subcommands", + description="List of available subcommands.", + ) + + for command, message, function in sorted(_iter_subcommands(), key=operator.itemgetter(0)): + sub1 = subparsers.add_parser(command, help=message, add_help=False) + sub1.add_argument('remainder', nargs=argparse.REMAINDER) + sub1.set_defaults(function=function) + + return parser + +def main(): + """Main function""" + + parser = commandline_parser() + args = parser.parse_args() + if hasattr(args, "function"): + args.function(args.remainder) + else: + parser.error("Missing command.") + +if __name__ == "__main__": + main() diff --git a/patacrep/tools/cache/__init__.py b/patacrep/tools/cache/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/patacrep/tools/cache/__main__.py b/patacrep/tools/cache/__main__.py new file mode 100644 index 00000000..de3ad0af --- /dev/null +++ b/patacrep/tools/cache/__main__.py @@ -0,0 +1,73 @@ +"""`patatools cache` command: cache manipulation.""" + +import argparse +import logging +import os +import shutil +import sys +import textwrap + +from patacrep import errors +from patacrep import songbook + +LOGGER = logging.getLogger("patatools.cache") +SUBCOMMAND_DESCRIPTION = "Perform operations on cache." + +def filename(name): + """Check that argument is an existing, readable file name. + + Return the argument for convenience. + """ + if os.path.isfile(name) and os.access(name, os.R_OK): + return name + raise argparse.ArgumentTypeError("Cannot read file '{}'.".format(name)) + +def commandline_parser(): + """Return a command line parser.""" + + parser = argparse.ArgumentParser( + prog="patatools cache", + description=SUBCOMMAND_DESCRIPTION, + formatter_class=argparse.RawTextHelpFormatter, + ) + + subparsers = parser.add_subparsers( + description="", + dest="command", + ) + subparsers.required = True + + clear = subparsers.add_parser( + "clear", + description="Delete cache.", + help="Delete cache.", + ) + clear.add_argument( + 'songbook', + metavar="SONGBOOK", + help=textwrap.dedent("""Songbook file to used to look for cache path."""), + type=filename, + ) + clear.set_defaults(command=do_clear) + + return parser + +def do_clear(namespace): + """Execute the `patatools cache clear` command.""" + for datadir in songbook.open_songbook(namespace.songbook)['datadir']: + cachedir = os.path.join(datadir, ".cache") + LOGGER.info("Deleting cache directory '{}'...".format(cachedir)) + if os.path.isdir(cachedir): + shutil.rmtree(cachedir) + +def main(args=None): + """Main function: run from command line.""" + options = commandline_parser().parse_args(args) + try: + options.command(options) + except errors.SongbookError as error: + LOGGER.error(str(error)) + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/patatools b/patatools new file mode 100755 index 00000000..0afec5a7 --- /dev/null +++ b/patatools @@ -0,0 +1,6 @@ +#! /bin/sh + +# Do not edit this file. This file is just a helper file for development test. +# It is not part of the distributed software. + +python -m patacrep.tools $@ diff --git a/setup.py b/setup.py index fd1b48a5..4d0c74c5 100755 --- a/setup.py +++ b/setup.py @@ -39,6 +39,7 @@ setup( entry_points={ 'console_scripts': [ "songbook = patacrep.songbook.__main__:main", + "patatools = patacrep.tools.__main__:main", ], }, classifiers=[ diff --git a/songbook b/songbook index bba8cbdf..f678cd88 100755 --- a/songbook +++ b/songbook @@ -3,8 +3,4 @@ # Do not edit this file. This file is just a helper file for development test. # It is not part of the distributed software. -if [ $1 = "purgecache" ]; then - find . -name ".cache" -type d -print -exec rm -r {} + -else - python -m patacrep.songbook $* -fi +python -m patacrep.songbook $@