From 4571bef5e5dc1fe9426b2c41ce291b31bd5a7ce9 Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 9 Jan 2016 22:57:12 +0100 Subject: [PATCH 01/31] New binary patatools --- patacrep/build.py | 8 +-- patacrep/errors.py | 2 +- patacrep/songbook/__init__.py | 54 ++++++++++++++ patacrep/songbook/__main__.py | 55 +++------------ patacrep/tools/__init__.py | 0 patacrep/tools/__main__.py | 116 +++++++++++++++++++++++++++++++ patacrep/tools/cache/__init__.py | 0 patacrep/tools/cache/__main__.py | 73 +++++++++++++++++++ patatools | 6 ++ setup.py | 1 + songbook | 6 +- 11 files changed, 266 insertions(+), 55 deletions(-) create mode 100644 patacrep/tools/__init__.py create mode 100644 patacrep/tools/__main__.py create mode 100644 patacrep/tools/cache/__init__.py create mode 100644 patacrep/tools/cache/__main__.py create mode 100755 patatools 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 $@ From 9be3072e4e367f0929987b83a74dabf5f32a5af0 Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 9 Jan 2016 23:11:07 +0100 Subject: [PATCH 02/31] Turn `patacrep.songs.convert` script into a `patatools` sub-command --- patacrep/tools/convert/__init__.py | 0 patacrep/{songs => tools}/convert/__main__.py | 31 +++++++++++-------- 2 files changed, 18 insertions(+), 13 deletions(-) create mode 100644 patacrep/tools/convert/__init__.py rename patacrep/{songs => tools}/convert/__main__.py (75%) diff --git a/patacrep/tools/convert/__init__.py b/patacrep/tools/convert/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/patacrep/songs/convert/__main__.py b/patacrep/tools/convert/__main__.py similarity index 75% rename from patacrep/songs/convert/__main__.py rename to patacrep/tools/convert/__main__.py index 19d7041e..2d308b4a 100644 --- a/patacrep/songs/convert/__main__.py +++ b/patacrep/tools/convert/__main__.py @@ -1,7 +1,4 @@ -"""Conversion between formats - -See the :meth:`__usage` method for more information. -""" +"""`patatools.convent` command: convert between song formats""" import os import logging @@ -11,27 +8,32 @@ from patacrep import files from patacrep.songs import DEFAULT_CONFIG from patacrep.utils import yesno -LOGGER = logging.getLogger(__name__) +LOGGER = logging.getLogger("patatools.convert") +SUBCOMMAND_DESCRIPTION = "Convert between song formats" -def __usage(): - return "python3 -m patacrep.songs.convert INPUTFORMAT OUTPUTFORMAT FILES" +def _usage(): + return "patatools convert INPUTFORMAT OUTPUTFORMAT FILES" def confirm(destname): + """Ask whether destination name should be overwrited.""" while True: try: return yesno(input("File '{}' already exist. Overwrite? [yn] ".format(destname))) except ValueError: continue -if __name__ == "__main__": - if len(sys.argv) < 4: +def main(args=None): + """Main function: run from command line.""" + if args is None: + args = sys.argv[1:] + if len(args) < 3: LOGGER.error("Invalid number of arguments.") - LOGGER.error("Usage: %s", __usage()) + LOGGER.error("Usage: %s", _usage()) sys.exit(1) - source = sys.argv[1] - dest = sys.argv[2] - song_files = sys.argv[3:] + source = args[0] + dest = args[1] + song_files = args[2:] renderers = files.load_plugins( datadirs=DEFAULT_CONFIG.get('datadir', []), @@ -73,3 +75,6 @@ if __name__ == "__main__": sys.exit(0) sys.exit(0) + +if __name__ == "__main__": + main() From 6f8b4fd6f9ba764024c9f9da61169ede0bc1a113 Mon Sep 17 00:00:00 2001 From: Louis Date: Sun, 10 Jan 2016 10:50:45 +0100 Subject: [PATCH 03/31] [patatools] Use built-in features to raise error when no subcommand is provided --- patacrep/tools/__main__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/patacrep/tools/__main__.py b/patacrep/tools/__main__.py index 17f3c8a4..a1aa0515 100644 --- a/patacrep/tools/__main__.py +++ b/patacrep/tools/__main__.py @@ -94,6 +94,8 @@ def commandline_parser(): title="Subcommands", description="List of available subcommands.", ) + subparsers.required = True + subparsers.dest = "subcommand" for command, message, function in sorted(_iter_subcommands(), key=operator.itemgetter(0)): sub1 = subparsers.add_parser(command, help=message, add_help=False) @@ -107,10 +109,7 @@ def main(): parser = commandline_parser() args = parser.parse_args() - if hasattr(args, "function"): - args.function(args.remainder) - else: - parser.error("Missing command.") + args.function(args.remainder) if __name__ == "__main__": main() From 9de0f01243989fa71a258d2dea6dbf2a22dbad35 Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 16 Jan 2016 09:31:31 +0100 Subject: [PATCH 04/31] Fix wrong logger name --- patacrep/songbook/__main__.py | 2 +- patacrep/tools/__main__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/patacrep/songbook/__main__.py b/patacrep/songbook/__main__.py index ed2364ff..daafc9f5 100644 --- a/patacrep/songbook/__main__.py +++ b/patacrep/songbook/__main__.py @@ -14,7 +14,7 @@ import patacrep.encoding # Logging configuration logging.basicConfig(level=logging.INFO) -LOGGER = logging.getLogger("patatools") +LOGGER = logging.getLogger() # pylint: disable=too-few-public-methods class ParseStepsAction(argparse.Action): diff --git a/patacrep/tools/__main__.py b/patacrep/tools/__main__.py index a1aa0515..d9202633 100644 --- a/patacrep/tools/__main__.py +++ b/patacrep/tools/__main__.py @@ -14,7 +14,7 @@ import patacrep # Logging configuration logging.basicConfig(level=logging.INFO) -LOGGER = logging.getLogger() +LOGGER = logging.getLogger("patatools") def _execlp(program, args): """Call :func:`os.execlp`, adding `program` as the first argument to itself.""" From ad5d103f3083715302684271f6e8ad1eea3c4b76 Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 16 Jan 2016 09:32:50 +0100 Subject: [PATCH 05/31] typo --- patacrep/tools/cache/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patacrep/tools/cache/__main__.py b/patacrep/tools/cache/__main__.py index de3ad0af..8dc2ee07 100644 --- a/patacrep/tools/cache/__main__.py +++ b/patacrep/tools/cache/__main__.py @@ -45,7 +45,7 @@ def commandline_parser(): clear.add_argument( 'songbook', metavar="SONGBOOK", - help=textwrap.dedent("""Songbook file to used to look for cache path."""), + help=textwrap.dedent("""Songbook file to be used to look for cache path."""), type=filename, ) clear.set_defaults(command=do_clear) From bd94153bc4892f282287b0bf828572b63472db05 Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 16 Jan 2016 23:34:43 +0100 Subject: [PATCH 06/31] [WIP] Test of patatools commands --- test/test_patatools/.gitignore | 1 + test/test_patatools/__init__.py | 0 test/test_patatools/test_cache.py | 41 +++++++++++++++++++ test/test_patatools/test_cache.sb | 3 ++ .../test_cache_datadir/songs/foo.csg | 0 test/test_patatools/test_convert.py | 0 6 files changed, 45 insertions(+) create mode 100644 test/test_patatools/.gitignore create mode 100644 test/test_patatools/__init__.py create mode 100644 test/test_patatools/test_cache.py create mode 100644 test/test_patatools/test_cache.sb create mode 100644 test/test_patatools/test_cache_datadir/songs/foo.csg create mode 100644 test/test_patatools/test_convert.py diff --git a/test/test_patatools/.gitignore b/test/test_patatools/.gitignore new file mode 100644 index 00000000..16d3c4db --- /dev/null +++ b/test/test_patatools/.gitignore @@ -0,0 +1 @@ +.cache diff --git a/test/test_patatools/__init__.py b/test/test_patatools/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/test_patatools/test_cache.py b/test/test_patatools/test_cache.py new file mode 100644 index 00000000..daa47af1 --- /dev/null +++ b/test/test_patatools/test_cache.py @@ -0,0 +1,41 @@ +"""Tests of the patatools-convert command.""" + +# pylint: disable=too-few-public-methods + +import os +import shutil +import unittest + +from patacrep.tools import convert + +CACHEDIR = os.path.join(os.path.dirname(__file__), "test_cache_datadir", "songs", ".cache") + +class TestCache(unittest.TestCase): + """Test of the "patatools cache" subcommand""" + + def setUp(self): + """Remove cache.""" + self._remove_cache() + + def tearDown(self): + """Remove cache.""" + self._remove_cache() + + def _remove_cache(self): + """Delete cache.""" + shutil.rmtree(CACHEDIR, ignore_errors=True) + + def test_clean(self): + """Test of the "patatools cache clean" subcommand""" + # Cache does not exist + self.assertFalse(os.path.exists(CACHEDIR)) + + # First compilation. Ensure that cache exists afterwards + TODO + self.assertTrue(os.path.exists(CACHEDIR)) + + # Clean cache + TODO + + # Ensure that cache does not exist + self.assertFalse(os.path.exists(CACHEDIR)) diff --git a/test/test_patatools/test_cache.sb b/test/test_patatools/test_cache.sb new file mode 100644 index 00000000..a7f15748 --- /dev/null +++ b/test/test_patatools/test_cache.sb @@ -0,0 +1,3 @@ +{ +"datadir": ["test_cache_datadir"], +} diff --git a/test/test_patatools/test_cache_datadir/songs/foo.csg b/test/test_patatools/test_cache_datadir/songs/foo.csg new file mode 100644 index 00000000..e69de29b diff --git a/test/test_patatools/test_convert.py b/test/test_patatools/test_convert.py new file mode 100644 index 00000000..e69de29b From bdb033c20bcb316d9b61c72282ad62fa8c576070 Mon Sep 17 00:00:00 2001 From: Louis Date: Sun, 17 Jan 2016 19:21:35 +0100 Subject: [PATCH 07/31] `patatools cache` improvements - Better handling of arguments - Tests --- patacrep/songbook/__main__.py | 7 ++--- patacrep/tools/__main__.py | 9 +++--- patacrep/tools/cache/__main__.py | 18 +++++------ test/test_patatools/test_cache.py | 52 +++++++++++++++++++++++-------- 4 files changed, 55 insertions(+), 31 deletions(-) diff --git a/patacrep/songbook/__main__.py b/patacrep/songbook/__main__.py index daafc9f5..bac1c5ea 100644 --- a/patacrep/songbook/__main__.py +++ b/patacrep/songbook/__main__.py @@ -113,9 +113,8 @@ def argument_parser(args): return options -def main(): +def main(args): """Main function:""" - # set script locale to match user's try: locale.setlocale(locale.LC_ALL, '') @@ -123,7 +122,7 @@ def main(): # Locale is not installed on user's system, or wrongly configured. LOGGER.error("Locale error: {}\n".format(str(error))) - options = argument_parser(sys.argv[1:]) + options = argument_parser(args[1:]) try: songbook = patacrep.songbook.open_songbook(options.book[-1]) @@ -152,4 +151,4 @@ def main(): sys.exit(0) if __name__ == '__main__': - main() + main(sys.argv) diff --git a/patacrep/tools/__main__.py b/patacrep/tools/__main__.py index d9202633..669dcec7 100644 --- a/patacrep/tools/__main__.py +++ b/patacrep/tools/__main__.py @@ -104,12 +104,11 @@ def commandline_parser(): return parser -def main(): +def main(args): """Main function""" - parser = commandline_parser() - args = parser.parse_args() - args.function(args.remainder) + args = parser.parse_args(args[1:]) + args.function(["patatools-{}".format(args.subcommand)] + args.remainder) if __name__ == "__main__": - main() + main(sys.argv) diff --git a/patacrep/tools/cache/__main__.py b/patacrep/tools/cache/__main__.py index 8dc2ee07..4aa8b2ec 100644 --- a/patacrep/tools/cache/__main__.py +++ b/patacrep/tools/cache/__main__.py @@ -37,32 +37,32 @@ def commandline_parser(): ) subparsers.required = True - clear = subparsers.add_parser( - "clear", + clean = subparsers.add_parser( + "clean", description="Delete cache.", help="Delete cache.", ) - clear.add_argument( + clean.add_argument( 'songbook', metavar="SONGBOOK", help=textwrap.dedent("""Songbook file to be used to look for cache path."""), type=filename, ) - clear.set_defaults(command=do_clear) + clean.set_defaults(command=do_clean) return parser -def do_clear(namespace): - """Execute the `patatools cache clear` command.""" +def do_clean(namespace): + """Execute the `patatools cache clean` 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): +def main(args): """Main function: run from command line.""" - options = commandline_parser().parse_args(args) + options = commandline_parser().parse_args(args[1:]) try: options.command(options) except errors.SongbookError as error: @@ -70,4 +70,4 @@ def main(args=None): sys.exit(1) if __name__ == "__main__": - main() + main(sys.argv) diff --git a/test/test_patatools/test_cache.py b/test/test_patatools/test_cache.py index daa47af1..6b22a74d 100644 --- a/test/test_patatools/test_cache.py +++ b/test/test_patatools/test_cache.py @@ -6,9 +6,12 @@ import os import shutil import unittest -from patacrep.tools import convert +from patacrep.files import chdir +from patacrep.tools.__main__ import main as tools_main +from patacrep.tools.cache.__main__ import main as cache_main +from patacrep.songbook.__main__ import main as songbook_main -CACHEDIR = os.path.join(os.path.dirname(__file__), "test_cache_datadir", "songs", ".cache") +CACHEDIR = os.path.join(os.path.dirname(__file__), "test_cache_datadir", ".cache") class TestCache(unittest.TestCase): """Test of the "patatools cache" subcommand""" @@ -16,26 +19,49 @@ class TestCache(unittest.TestCase): def setUp(self): """Remove cache.""" self._remove_cache() + self.assertFalse(os.path.exists(CACHEDIR)) def tearDown(self): """Remove cache.""" self._remove_cache() + self.assertFalse(os.path.exists(CACHEDIR)) - def _remove_cache(self): + @staticmethod + def _remove_cache(): """Delete cache.""" shutil.rmtree(CACHEDIR, ignore_errors=True) - def test_clean(self): + def _system(self, main, args): + with chdir(os.path.dirname(__file__)): + try: + main(args) + except SystemExit as systemexit: + self.assertEqual(systemexit.code, 0) + + def test_clean_exists(self): """Test of the "patatools cache clean" subcommand""" - # Cache does not exist - self.assertFalse(os.path.exists(CACHEDIR)) + for main, args in [ + (tools_main, ["patatools", "cache", "clean", "test_cache.sb"]), + (cache_main, ["patatools-cache", "clean", "test_cache.sb"]), + ]: + with self.subTest(main=main, args=args): + # First compilation. Ensure that cache exists afterwards + self._system(songbook_main, ["songbook", "test_cache.sb"]) + self.assertTrue(os.path.exists(CACHEDIR)) - # First compilation. Ensure that cache exists afterwards - TODO - self.assertTrue(os.path.exists(CACHEDIR)) + # Clean cache + self._system(main, args) - # Clean cache - TODO + # Ensure that cache does not exist + self.assertFalse(os.path.exists(CACHEDIR)) - # Ensure that cache does not exist - self.assertFalse(os.path.exists(CACHEDIR)) + def test_clean_not_exists(self): + """Test of the "patatools cache clean" subcommand""" + # Clean non-existent cache + for main, args in [ + (tools_main, ["patatools", "cache", "clean", "test_cache.sb"]), + (cache_main, ["patatools-cache", "clean", "test_cache.sb"]), + ]: + with self.subTest(main=main, args=args): + # Clean cache + self._system(main, args) From 76bd6654151f8d5d51e7893414043d33e1013d2b Mon Sep 17 00:00:00 2001 From: Louis Date: Mon, 18 Jan 2016 19:00:20 +0100 Subject: [PATCH 08/31] Fix import errors --- patacrep/songbook/__main__.py | 3 ++- patacrep/tools/cache/__main__.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/patacrep/songbook/__main__.py b/patacrep/songbook/__main__.py index bac1c5ea..5c75084b 100644 --- a/patacrep/songbook/__main__.py +++ b/patacrep/songbook/__main__.py @@ -8,6 +8,7 @@ import textwrap from patacrep import __version__ from patacrep import errors +from patacrep.songbook import open_songbook from patacrep.build import SongbookBuilder, DEFAULT_STEPS from patacrep.utils import yesno import patacrep.encoding @@ -125,7 +126,7 @@ def main(args): options = argument_parser(args[1:]) try: - songbook = patacrep.songbook.open_songbook(options.book[-1]) + songbook = open_songbook(options.book[-1]) # Command line options if options.datadir: diff --git a/patacrep/tools/cache/__main__.py b/patacrep/tools/cache/__main__.py index 4aa8b2ec..22bce3fc 100644 --- a/patacrep/tools/cache/__main__.py +++ b/patacrep/tools/cache/__main__.py @@ -8,7 +8,7 @@ import sys import textwrap from patacrep import errors -from patacrep import songbook +from patacrep.songbook import open_songbook LOGGER = logging.getLogger("patatools.cache") SUBCOMMAND_DESCRIPTION = "Perform operations on cache." @@ -54,7 +54,7 @@ def commandline_parser(): def do_clean(namespace): """Execute the `patatools cache clean` command.""" - for datadir in songbook.open_songbook(namespace.songbook)['datadir']: + for datadir in open_songbook(namespace.songbook)['datadir']: cachedir = os.path.join(datadir, ".cache") LOGGER.info("Deleting cache directory '{}'...".format(cachedir)) if os.path.isdir(cachedir): From 74b0649a62b7ddbc077fdf3078b6bffcb21b609f Mon Sep 17 00:00:00 2001 From: Louis Date: Mon, 18 Jan 2016 19:04:45 +0100 Subject: [PATCH 09/31] [test] Do not compile songbook during "patatools-cache" test Test is faster, and works on Travis (where lualatex is not installed) --- test/test_patatools/test_cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_patatools/test_cache.py b/test/test_patatools/test_cache.py index 6b22a74d..3e1b6576 100644 --- a/test/test_patatools/test_cache.py +++ b/test/test_patatools/test_cache.py @@ -46,7 +46,7 @@ class TestCache(unittest.TestCase): ]: with self.subTest(main=main, args=args): # First compilation. Ensure that cache exists afterwards - self._system(songbook_main, ["songbook", "test_cache.sb"]) + self._system(songbook_main, ["songbook", "--steps", "tex,clean", "test_cache.sb"]) self.assertTrue(os.path.exists(CACHEDIR)) # Clean cache From d6d02d392f8a1462cad019ae946c393a5798ed78 Mon Sep 17 00:00:00 2001 From: Louis Date: Mon, 18 Jan 2016 19:09:37 +0100 Subject: [PATCH 10/31] Remove useless import --- patacrep/songbook/__main__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/patacrep/songbook/__main__.py b/patacrep/songbook/__main__.py index 5c75084b..3338b099 100644 --- a/patacrep/songbook/__main__.py +++ b/patacrep/songbook/__main__.py @@ -11,7 +11,6 @@ from patacrep import errors from patacrep.songbook import open_songbook from patacrep.build import SongbookBuilder, DEFAULT_STEPS from patacrep.utils import yesno -import patacrep.encoding # Logging configuration logging.basicConfig(level=logging.INFO) From 002328e1b545832963b389b5e1f66d286992d598 Mon Sep 17 00:00:00 2001 From: Louis Date: Mon, 18 Jan 2016 19:09:45 +0100 Subject: [PATCH 11/31] [DEBUG] Debug travis error --- patacrep/tools/cache/__main__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/patacrep/tools/cache/__main__.py b/patacrep/tools/cache/__main__.py index 22bce3fc..28bffb43 100644 --- a/patacrep/tools/cache/__main__.py +++ b/patacrep/tools/cache/__main__.py @@ -64,6 +64,8 @@ def main(args): """Main function: run from command line.""" options = commandline_parser().parse_args(args[1:]) try: + print(80*"#") + print(options.command, type(options.command)) options.command(options) except errors.SongbookError as error: LOGGER.error(str(error)) From 38276d5289b97d9e404778a481251d167302c98b Mon Sep 17 00:00:00 2001 From: oliverpool Date: Mon, 18 Jan 2016 21:05:27 +0100 Subject: [PATCH 12/31] [debug] Try travis with python 3.5 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c398bbfe..417a9dc7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ git: depth: 1 language: python python: - - 3.4 + - 3.5 install: - pip install tox script: From 9d17307b5a17557861bfb3d83de58868443670a6 Mon Sep 17 00:00:00 2001 From: oliverpool Date: Mon, 18 Jan 2016 21:10:34 +0100 Subject: [PATCH 13/31] [debug] force tox to use py35 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 417a9dc7..84d19191 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ python: install: - pip install tox script: - - tox + - tox -e py35 sudo: false # addons: # apt: From e2b07943dfe09766be8e262c743dbfb6afae0940 Mon Sep 17 00:00:00 2001 From: oliverpool Date: Mon, 18 Jan 2016 21:12:29 +0100 Subject: [PATCH 14/31] [debug] travis should also run lint --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 84d19191..fd870b0b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,7 @@ python: install: - pip install tox script: + - tox -e lint - tox -e py35 sudo: false # addons: From cb04843e9b30872dfb68ad42e5cf3c87bdbb23bf Mon Sep 17 00:00:00 2001 From: oliverpool Date: Mon, 18 Jan 2016 21:14:55 +0100 Subject: [PATCH 15/31] [debug] run lint and py35 in one command --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index fd870b0b..0f4e5c8d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,8 +6,7 @@ python: install: - pip install tox script: - - tox -e lint - - tox -e py35 + - tox -e lint,py35 sudo: false # addons: # apt: From 8281454b6b485f4d7ccabf53562d946733d5533c Mon Sep 17 00:00:00 2001 From: oliverpool Date: Mon, 18 Jan 2016 21:30:01 +0100 Subject: [PATCH 16/31] typo --- patacrep/tools/convert/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patacrep/tools/convert/__main__.py b/patacrep/tools/convert/__main__.py index 2d308b4a..3e84ea1a 100644 --- a/patacrep/tools/convert/__main__.py +++ b/patacrep/tools/convert/__main__.py @@ -1,4 +1,4 @@ -"""`patatools.convent` command: convert between song formats""" +"""`patatools.convert` command: convert between song formats""" import os import logging From 6bfd033abc204f7c7b9d584070b982146cf5731b Mon Sep 17 00:00:00 2001 From: Louis Date: Mon, 18 Jan 2016 21:47:52 +0100 Subject: [PATCH 17/31] [debug] Remove debugging traces --- patacrep/tools/cache/__main__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/patacrep/tools/cache/__main__.py b/patacrep/tools/cache/__main__.py index 28bffb43..22bce3fc 100644 --- a/patacrep/tools/cache/__main__.py +++ b/patacrep/tools/cache/__main__.py @@ -64,8 +64,6 @@ def main(args): """Main function: run from command line.""" options = commandline_parser().parse_args(args[1:]) try: - print(80*"#") - print(options.command, type(options.command)) options.command(options) except errors.SongbookError as error: LOGGER.error(str(error)) From 9431f93b704686df7fc9603dbfbd2db239d8b20a Mon Sep 17 00:00:00 2001 From: Louis Date: Wed, 27 Jan 2016 23:23:36 +0100 Subject: [PATCH 18/31] typo --- test/test_patatools/test_cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_patatools/test_cache.py b/test/test_patatools/test_cache.py index 3e1b6576..37a98689 100644 --- a/test/test_patatools/test_cache.py +++ b/test/test_patatools/test_cache.py @@ -1,4 +1,4 @@ -"""Tests of the patatools-convert command.""" +"""Tests of the patatools-cache command.""" # pylint: disable=too-few-public-methods From 0e54d18ab17f99700474ab71a289e643fdc14961 Mon Sep 17 00:00:00 2001 From: Louis Date: Thu, 28 Jan 2016 15:49:22 +0100 Subject: [PATCH 19/31] [WIP] Test of `patatools convert FOO` works - Errors are not tested yet - Pylint may fail --- patacrep/tools/convert/__main__.py | 10 +-- test/test_patatools/test_convert.py | 85 +++++++++++++++++++ .../test_convert_errors/song.tsg | 12 +++ .../test_convert_success/.gitignore | 1 + .../test_convert_success/greensleeves.csg | 45 ++++++++++ .../greensleeves.csg.tsg.control | 62 ++++++++++++++ 6 files changed, 210 insertions(+), 5 deletions(-) create mode 100644 test/test_patatools/test_convert_errors/song.tsg create mode 100644 test/test_patatools/test_convert_success/.gitignore create mode 100644 test/test_patatools/test_convert_success/greensleeves.csg create mode 100644 test/test_patatools/test_convert_success/greensleeves.csg.tsg.control diff --git a/patacrep/tools/convert/__main__.py b/patacrep/tools/convert/__main__.py index 3e84ea1a..f64badc9 100644 --- a/patacrep/tools/convert/__main__.py +++ b/patacrep/tools/convert/__main__.py @@ -25,15 +25,15 @@ def confirm(destname): def main(args=None): """Main function: run from command line.""" if args is None: - args = sys.argv[1:] - if len(args) < 3: + args = sys.argv + if len(args) < 4: LOGGER.error("Invalid number of arguments.") LOGGER.error("Usage: %s", _usage()) sys.exit(1) - source = args[0] - dest = args[1] - song_files = args[2:] + source = args[1] + dest = args[2] + song_files = args[3:] renderers = files.load_plugins( datadirs=DEFAULT_CONFIG.get('datadir', []), diff --git a/test/test_patatools/test_convert.py b/test/test_patatools/test_convert.py index e69de29b..38ee9995 100644 --- a/test/test_patatools/test_convert.py +++ b/test/test_patatools/test_convert.py @@ -0,0 +1,85 @@ +"""Tests of the patatools-convert command.""" + +# pylint: disable=too-few-public-methods + +from pkg_resources import resource_filename +import contextlib +import glob +import os +import shutil +import unittest + +from patacrep import files +from patacrep.tools.__main__ import main as tools_main +from patacrep.encoding import open_read +from patacrep.tools.convert.__main__ import main as convert_main +from patacrep.songbook.__main__ import main as songbook_main + +from .. import dynamic +from .. import logging_reduced + +class TestConvert(unittest.TestCase, metaclass=dynamic.DynamicTest): + """Test of the "patatools convert" subcommand""" + + def _system(self, main, args): + try: + main(args) + except SystemExit as systemexit: + return systemexit.code + return 0 + + def assertConvert(self, basename, in_format, out_format): + """Test of the "patatools convert" subcommand""" + sourcename = "{}.{}".format(basename, in_format) + destname = "{}.{}".format(basename, out_format) + controlname = "{}.{}.control".format(sourcename, out_format) + for main, args in [ + (tools_main, ["patatools", "convert"]), + (convert_main, ["patatools-convert"]), + ]: + with self.subTest(main=main, args=args): + with self.chdir("test_convert_success"): + with open_read(controlname) as controlfile: + with logging_reduced(): + if os.path.exists(destname): + os.remove(destname) + self._system(main, args + [in_format, out_format, sourcename]) + expected = controlfile.read().strip().replace( + "@TEST_FOLDER@", + files.path2posix(resource_filename(__name__, "")), + ) + with open_read(destname) as destfile: + self.assertMultiLineEqual( + destfile.read().strip(), + expected, + ) + + @staticmethod + @contextlib.contextmanager + def chdir(*path): + """Context to temporarry change current directory, relative to this file directory + """ + with files.chdir(resource_filename(__name__, ""), *path): + yield + + @classmethod + def _iter_testmethods(cls): + """Iterate over song files to test.""" + with cls.chdir("test_convert_success"): + for control in sorted(glob.glob('*.*.*.control')): + [*base, in_format, out_format, _] = control.split('.') + base = '.'.join(base) + yield ( + "test_{}_{}_{}".format(base, in_format, out_format), + cls._create_test(base, in_format, out_format), + ) + + @classmethod + def _create_test(cls, base, in_format, out_format): + """Return a function testing that `base` compilation from `in_format` to `out_format` format. + """ + test_parse_render = lambda self: self.assertConvert(base, in_format, out_format) + test_parse_render.__doc__ = ( + "Test that '{base}.{in_format}' is correctly converted to '{out_format}'." + ).format(base=os.path.basename(base), in_format=in_format, out_format=out_format) + return test_parse_render diff --git a/test/test_patatools/test_convert_errors/song.tsg b/test/test_patatools/test_convert_errors/song.tsg new file mode 100644 index 00000000..c57d0f79 --- /dev/null +++ b/test/test_patatools/test_convert_errors/song.tsg @@ -0,0 +1,12 @@ +\beginsong{Wonderful song} + [by={Anonymous}] + + \begin{verse} + \[A]La la la\\ + \end{verse} + + \begin{chorus} + \[C]La la la\\ + \end{chorus} + +\endsong diff --git a/test/test_patatools/test_convert_success/.gitignore b/test/test_patatools/test_convert_success/.gitignore new file mode 100644 index 00000000..9947c79b --- /dev/null +++ b/test/test_patatools/test_convert_success/.gitignore @@ -0,0 +1 @@ +greensleeves.tsg diff --git a/test/test_patatools/test_convert_success/greensleeves.csg b/test/test_patatools/test_convert_success/greensleeves.csg new file mode 100644 index 00000000..54f8911f --- /dev/null +++ b/test/test_patatools/test_convert_success/greensleeves.csg @@ -0,0 +1,45 @@ +{lang: en} +{columns: 2} +{title: Greensleeves} +{title: Un autre sous-titre} +{title: Un sous titre} +{artist: Traditionnel} +{album: Angleterre} + + +A[Am]las, my love, ye [G]do me wrong +To [Am]cast me oft dis[E]curteously +And [Am]I have loved [G]you so long +De[Am]lighting [E]in your [Am]companie + + +{start_of_chorus} + [C]Green[B]sleeves was [G]all my joy + [Am]Greensleeves was [E]my delight + [C]Greensleeves was my [G]heart of gold + And [Am]who but [E]Ladie [Am]Greensleeves +{end_of_chorus} + + +I [Am]have been ready [G]at your hand +To [Am]grant what ever [E]you would crave +I [Am]have both waged [G]life and land +Your [Am]love and [E]good will [Am]for to have + + +I [Am]bought thee kerchers [G]to thy head +That [Am]were wrought fine and [E]gallantly +I [Am]kept thee both at [G]boord and bed +Which [Am]cost my [E]purse well [Am]favouredly + + +I [Am]bought thee peticotes [G]of the best +The [Am]cloth so fine as [E]fine might be +I [Am]gave thee jewels [G]for thy chest +And [Am]all this [E]cost I [Am]spent on thee + + +Thy [Am]smock of silke, both [G]faire and white +With [Am]gold embrodered [E]gorgeously +Thy [Am]peticote of [G]sendall right +And [Am]this I [E]bought thee [Am]gladly diff --git a/test/test_patatools/test_convert_success/greensleeves.csg.tsg.control b/test/test_patatools/test_convert_success/greensleeves.csg.tsg.control new file mode 100644 index 00000000..cba09096 --- /dev/null +++ b/test/test_patatools/test_convert_success/greensleeves.csg.tsg.control @@ -0,0 +1,62 @@ +\selectlanguage{english} +\songcolumns{2} + +\beginsong{Greensleeves\\ +Un autre sous-titre\\ +Un sous titre}[ + by={ + Traditionnel }, + album={Angleterre}, +] + + + + +\begin{verse} + A\[Am]las, my love, ye \[G]do me wrong + To \[Am]cast me oft dis\[E]curteously + And \[Am]I have loved \[G]you so long + De\[Am]lighting \[E]in your \[Am]companie +\end{verse} + + +\begin{chorus} + \[C]Green\[B]sleeves was \[G]all my joy + \[Am]Greensleeves was \[E]my delight + \[C]Greensleeves was my \[G]heart of gold + And \[Am]who but \[E]Ladie \[Am]Greensleeves +\end{chorus} + + +\begin{verse} + I \[Am]have been ready \[G]at your hand + To \[Am]grant what ever \[E]you would crave + I \[Am]have both waged \[G]life and land + Your \[Am]love and \[E]good will \[Am]for to have +\end{verse} + + +\begin{verse} + I \[Am]bought thee kerchers \[G]to thy head + That \[Am]were wrought fine and \[E]gallantly + I \[Am]kept thee both at \[G]boord and bed + Which \[Am]cost my \[E]purse well \[Am]favouredly +\end{verse} + + +\begin{verse} + I \[Am]bought thee peticotes \[G]of the best + The \[Am]cloth so fine as \[E]fine might be + I \[Am]gave thee jewels \[G]for thy chest + And \[Am]all this \[E]cost I \[Am]spent on thee +\end{verse} + + +\begin{verse} + Thy \[Am]smock of silke, both \[G]faire and white + With \[Am]gold embrodered \[E]gorgeously + Thy \[Am]peticote of \[G]sendall right + And \[Am]this I \[E]bought thee \[Am]gladly +\end{verse} + +\endsong From dbd8c1f5d0fb5393111fef11d9cfc6fa79b23d07 Mon Sep 17 00:00:00 2001 From: Louis Date: Thu, 28 Jan 2016 18:24:53 +0100 Subject: [PATCH 20/31] Better handling of song parsing errors of chordpro files Closes #191 --- patacrep/songs/chordpro/syntax.py | 8 +- patacrep/tools/convert/__main__.py | 6 +- test/test_patatools/test_convert.py | 57 ++++++-- .../test_convert_errors/song.tsg | 12 -- .../test_convert_failure/song.csg | 122 ++++++++++++++++++ .../test_convert_failure/song.csg.tsg.control | 0 6 files changed, 177 insertions(+), 28 deletions(-) delete mode 100644 test/test_patatools/test_convert_errors/song.tsg create mode 100644 test/test_patatools/test_convert_failure/song.csg create mode 100644 test/test_patatools/test_convert_failure/song.csg.tsg.control diff --git a/patacrep/songs/chordpro/syntax.py b/patacrep/songs/chordpro/syntax.py index 04104ac6..76d2a0d8 100644 --- a/patacrep/songs/chordpro/syntax.py +++ b/patacrep/songs/chordpro/syntax.py @@ -38,6 +38,8 @@ class ChordproParser(Parser): lexer = ChordProLexer(filename=self.filename) ast.AST.lexer = lexer.lexer parsed = self.parser.parse(content, lexer=lexer.lexer) + if parsed is None: + raise ContentError(message='Fatal error during song parsing.') parsed.error_builders.extend(lexer.error_builders) return parsed @@ -325,8 +327,4 @@ class ChordproParser(Parser): def parse_song(content, filename=None): """Parse song and return its metadata.""" - parser = ChordproParser(filename) - parsed_content = parser.parse(content) - if parsed_content is None: - raise ContentError(message='Fatal error during song parsing.') - return parsed_content + return ChordproParser(filename).parse(content) diff --git a/patacrep/tools/convert/__main__.py b/patacrep/tools/convert/__main__.py index f64badc9..683e8535 100644 --- a/patacrep/tools/convert/__main__.py +++ b/patacrep/tools/convert/__main__.py @@ -5,6 +5,7 @@ import logging import sys from patacrep import files +from patacrep.content import ContentError from patacrep.songs import DEFAULT_CONFIG from patacrep.utils import yesno @@ -57,8 +58,8 @@ def main(args=None): sys.exit(1) for file in song_files: - song = renderers[dest][source](file, DEFAULT_CONFIG) try: + song = renderers[dest][source](file, DEFAULT_CONFIG) destname = "{}.{}".format(".".join(file.split(".")[:-1]), dest) if os.path.exists(destname): if not confirm(destname): @@ -66,6 +67,9 @@ def main(args=None): with open(destname, "w") as destfile: destfile.write(song.render()) + except ContentError as error: + LOGGER.error("Cannot parse file '%s'.", file) + sys.exit(1) except NotImplementedError: LOGGER.error("Cannot convert to format '%s'.", dest) sys.exit(1) diff --git a/test/test_patatools/test_convert.py b/test/test_patatools/test_convert.py index 38ee9995..30c1dc24 100644 --- a/test/test_patatools/test_convert.py +++ b/test/test_patatools/test_convert.py @@ -43,7 +43,10 @@ class TestConvert(unittest.TestCase, metaclass=dynamic.DynamicTest): with logging_reduced(): if os.path.exists(destname): os.remove(destname) - self._system(main, args + [in_format, out_format, sourcename]) + self.assertEqual( + self._system(main, args + [in_format, out_format, sourcename]), + 0, + ) expected = controlfile.read().strip().replace( "@TEST_FOLDER@", files.path2posix(resource_filename(__name__, "")), @@ -54,6 +57,26 @@ class TestConvert(unittest.TestCase, metaclass=dynamic.DynamicTest): expected, ) + def assertFailConvert(self, basename, in_format, out_format): + """Test of the "patatools convert" subcommand""" + sourcename = "{}.{}".format(basename, in_format) + destname = "{}.{}".format(basename, out_format) + controlname = "{}.{}.control".format(sourcename, out_format) + for main, args in [ + (tools_main, ["patatools", "convert"]), + (convert_main, ["patatools-convert"]), + ]: + with self.subTest(main=main, args=args): + with self.chdir("test_convert_failure"): + with open_read(controlname) as controlfile: + with logging_reduced(): + if os.path.exists(destname): + os.remove(destname) + self.assertEqual( + self._system(main, args + [in_format, out_format, sourcename]), + 1, + ) + @staticmethod @contextlib.contextmanager def chdir(*path): @@ -65,17 +88,21 @@ class TestConvert(unittest.TestCase, metaclass=dynamic.DynamicTest): @classmethod def _iter_testmethods(cls): """Iterate over song files to test.""" - with cls.chdir("test_convert_success"): - for control in sorted(glob.glob('*.*.*.control')): - [*base, in_format, out_format, _] = control.split('.') - base = '.'.join(base) - yield ( - "test_{}_{}_{}".format(base, in_format, out_format), - cls._create_test(base, in_format, out_format), - ) + for directory, create_test in [ + ("test_convert_success", cls._create_test_success), + ("test_convert_failure", cls._create_test_failure), + ]: + with cls.chdir(directory): + for control in sorted(glob.glob('*.*.*.control')): + [*base, in_format, out_format, _] = control.split('.') + base = '.'.join(base) + yield ( + "test_{}_{}_{}".format(base, in_format, out_format), + create_test(base, in_format, out_format), + ) @classmethod - def _create_test(cls, base, in_format, out_format): + def _create_test_success(cls, base, in_format, out_format): """Return a function testing that `base` compilation from `in_format` to `out_format` format. """ test_parse_render = lambda self: self.assertConvert(base, in_format, out_format) @@ -83,3 +110,13 @@ class TestConvert(unittest.TestCase, metaclass=dynamic.DynamicTest): "Test that '{base}.{in_format}' is correctly converted to '{out_format}'." ).format(base=os.path.basename(base), in_format=in_format, out_format=out_format) return test_parse_render + + @classmethod + def _create_test_failure(cls, base, in_format, out_format): + """Return a function testing that `base` compilation from `in_format` to `out_format` format. + """ + test_parse_render = lambda self: self.assertFailConvert(base, in_format, out_format) + test_parse_render.__doc__ = ( + "Test that '{base}.{in_format}' raises an error when trying to convert it to '{out_format}'." + ).format(base=os.path.basename(base), in_format=in_format, out_format=out_format) + return test_parse_render diff --git a/test/test_patatools/test_convert_errors/song.tsg b/test/test_patatools/test_convert_errors/song.tsg deleted file mode 100644 index c57d0f79..00000000 --- a/test/test_patatools/test_convert_errors/song.tsg +++ /dev/null @@ -1,12 +0,0 @@ -\beginsong{Wonderful song} - [by={Anonymous}] - - \begin{verse} - \[A]La la la\\ - \end{verse} - - \begin{chorus} - \[C]La la la\\ - \end{chorus} - -\endsong diff --git a/test/test_patatools/test_convert_failure/song.csg b/test/test_patatools/test_convert_failure/song.csg new file mode 100644 index 00000000..cb763ba7 --- /dev/null +++ b/test/test_patatools/test_convert_failure/song.csg @@ -0,0 +1,122 @@ +\selectlanguage{english} + +\beginsong{}[ + by={ + }, +] + + +\begin{verse} + \selectlanguage + \songcolumns + \beginsong + \[&y={Traditionnel},cover={traditionnel},al&um={France}] +\end{verse} + + +\begin{verse} + \cover + \gtab + \gtab + \gtab +\end{verse} + + +\begin{verse} + \begin + Cheva\\[C]liers de la Table Ronde + Goûtons \\[G7]voir si le vin est \\[C]bon + \rep + \end +\end{verse} + + +\begin{verse} + \begin + Goûtons \\[F]voir, \echo + Goûtons \\[C]voir, \echo + Goûtons \\[G7]voir si le vin est bon + \rep + \end +\end{verse} + + +\begin{verse} + \begin + S'il est bon, s'il est agréable + J'en boirai jusqu'à mon plaisir + \end +\end{verse} + + +\begin{verse} + \begin + J'en boirai cinq à six bouteilles + Et encore, ce n'est pas beaucoup + \end +\end{verse} + + +\begin{verse} + \begin + Si je meurs, je veux qu'on m'enterre + Dans une cave où il y a du bon vin + \end +\end{verse} + + +\begin{verse} + \begin + Les deux pieds contre la muraille + Et la tête sous le robinet + \end +\end{verse} + + +\begin{verse} + \begin + Et les quatre plus grands ivrognes + Porteront les quatre coins du drap + \end +\end{verse} + + +\begin{verse} + \begin + Pour donner le discours d'usage + On prendra le bistrot du coin + \end +\end{verse} + + +\begin{verse} + \begin + Et si le tonneau se débouche + J'en boirai jusqu'à mon plaisir + \end +\end{verse} + + +\begin{verse} + \begin + Et s'il en reste quelques gouttes + Ce sera pour nous rafraîchir + \end +\end{verse} + + +\begin{verse} + \begin + Sur ma tombe, je veux qu'on inscrive + \emph + \end +\end{verse} + + +\begin{verse} + \endsong +\end{verse} + + + +\endsong \ No newline at end of file diff --git a/test/test_patatools/test_convert_failure/song.csg.tsg.control b/test/test_patatools/test_convert_failure/song.csg.tsg.control new file mode 100644 index 00000000..e69de29b From 1e1a25c2037b6e7f9b4685887f51a2fce37bf88d Mon Sep 17 00:00:00 2001 From: Louis Date: Thu, 28 Jan 2016 18:48:03 +0100 Subject: [PATCH 21/31] Pylint compliance --- patacrep/tools/convert/__main__.py | 2 +- test/test_patatools/test_convert.py | 47 +++++++++++++++-------------- test/test_song/test_parser.py | 3 +- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/patacrep/tools/convert/__main__.py b/patacrep/tools/convert/__main__.py index 683e8535..eae1f691 100644 --- a/patacrep/tools/convert/__main__.py +++ b/patacrep/tools/convert/__main__.py @@ -67,7 +67,7 @@ def main(args=None): with open(destname, "w") as destfile: destfile.write(song.render()) - except ContentError as error: + except ContentError: LOGGER.error("Cannot parse file '%s'.", file) sys.exit(1) except NotImplementedError: diff --git a/test/test_patatools/test_convert.py b/test/test_patatools/test_convert.py index 30c1dc24..5fa479b3 100644 --- a/test/test_patatools/test_convert.py +++ b/test/test_patatools/test_convert.py @@ -2,33 +2,33 @@ # pylint: disable=too-few-public-methods -from pkg_resources import resource_filename import contextlib import glob import os -import shutil import unittest +from pkg_resources import resource_filename + from patacrep import files from patacrep.tools.__main__ import main as tools_main from patacrep.encoding import open_read from patacrep.tools.convert.__main__ import main as convert_main -from patacrep.songbook.__main__ import main as songbook_main -from .. import dynamic +from .. import dynamic # pylint: disable=unused-import from .. import logging_reduced class TestConvert(unittest.TestCase, metaclass=dynamic.DynamicTest): """Test of the "patatools convert" subcommand""" - def _system(self, main, args): + @staticmethod + def _system(main, args): try: main(args) except SystemExit as systemexit: return systemexit.code return 0 - def assertConvert(self, basename, in_format, out_format): + def assertConvert(self, basename, in_format, out_format): # pylint: disable=invalid-name """Test of the "patatools convert" subcommand""" sourcename = "{}.{}".format(basename, in_format) destname = "{}.{}".format(basename, out_format) @@ -57,32 +57,29 @@ class TestConvert(unittest.TestCase, metaclass=dynamic.DynamicTest): expected, ) - def assertFailConvert(self, basename, in_format, out_format): + def assertFailConvert(self, basename, in_format, out_format): # pylint: disable=invalid-name """Test of the "patatools convert" subcommand""" sourcename = "{}.{}".format(basename, in_format) destname = "{}.{}".format(basename, out_format) - controlname = "{}.{}.control".format(sourcename, out_format) for main, args in [ (tools_main, ["patatools", "convert"]), (convert_main, ["patatools-convert"]), ]: with self.subTest(main=main, args=args): with self.chdir("test_convert_failure"): - with open_read(controlname) as controlfile: - with logging_reduced(): - if os.path.exists(destname): - os.remove(destname) - self.assertEqual( - self._system(main, args + [in_format, out_format, sourcename]), - 1, - ) + with logging_reduced(): + if os.path.exists(destname): + os.remove(destname) + self.assertEqual( + self._system(main, args + [in_format, out_format, sourcename]), + 1, + ) @staticmethod @contextlib.contextmanager - def chdir(*path): - """Context to temporarry change current directory, relative to this file directory - """ - with files.chdir(resource_filename(__name__, ""), *path): + def chdir(*pathlist): + """Temporary change current directory, relative to this file directory""" + with files.chdir(resource_filename(__name__, ""), *pathlist): yield @classmethod @@ -103,7 +100,11 @@ class TestConvert(unittest.TestCase, metaclass=dynamic.DynamicTest): @classmethod def _create_test_success(cls, base, in_format, out_format): - """Return a function testing that `base` compilation from `in_format` to `out_format` format. + """Return a function testing conversion. + + :param str base: Base name of the file to convert. + :param str in_format: Source format. + :param str out_format: Destination format. """ test_parse_render = lambda self: self.assertConvert(base, in_format, out_format) test_parse_render.__doc__ = ( @@ -113,10 +114,10 @@ class TestConvert(unittest.TestCase, metaclass=dynamic.DynamicTest): @classmethod def _create_test_failure(cls, base, in_format, out_format): - """Return a function testing that `base` compilation from `in_format` to `out_format` format. + """Return a function testing failing conversions """ test_parse_render = lambda self: self.assertFailConvert(base, in_format, out_format) test_parse_render.__doc__ = ( - "Test that '{base}.{in_format}' raises an error when trying to convert it to '{out_format}'." + "Test that '{base}.{in_format}' raises an error when trying to convert it to '{out_format}'." # pylint: disable=line-too-long ).format(base=os.path.basename(base), in_format=in_format, out_format=out_format) return test_parse_render diff --git a/test/test_song/test_parser.py b/test/test_song/test_parser.py index 298f0017..e660351a 100644 --- a/test/test_song/test_parser.py +++ b/test/test_song/test_parser.py @@ -48,8 +48,7 @@ class FileTest(unittest.TestCase, metaclass=dynamic.DynamicTest): @staticmethod @contextlib.contextmanager def chdir(*path): - """Context to temporarry change current directory, relative to this file directory - """ + """Temporary change current directory, relative to this file directory""" with files.chdir(resource_filename(__name__, ""), *path): yield From 82bb41d553ccd256055595765b7dea0673fdbf8f Mon Sep 17 00:00:00 2001 From: Louis Date: Thu, 28 Jan 2016 18:55:05 +0100 Subject: [PATCH 22/31] Appveyor update See 047198d16137f4c6054ea64864ae70b193436f2c. --- .appveyor.yml | 6 +++--- texlive_packages.txt | 4 ---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index ed692231..a403001d 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -23,7 +23,7 @@ install: - "python -c \"import struct; print(struct.calcsize('P') * 8)\"" # Download miktex portable (if not cached) - - ps: "If (!(Test-Path miktex-portable.exe)){wget http://mirrors.ctan.org/systems/win32/miktex/setup/miktex-portable-2.9.5719.exe -OutFile ./miktex-portable.exe}" + - ps: "If (!(Test-Path miktex-portable.exe)){wget http://mirrors.ctan.org/systems/win32/miktex/setup/miktex-portable-2.9.5857.exe -OutFile ./miktex-portable.exe}" # Unzip miktex portable - "7z x miktex-portable.exe * -aot -omiktex > nul" @@ -32,8 +32,8 @@ install: - cmd: set PATH=%PATH%;C:\projects\patacrep\miktex\miktex\bin # Update some packages to prevent ltluatex bug - - cmd: mpm.exe --update=miktex-bin-2.9 - - cmd: mpm.exe --update=ltxbase --update=luatexbase --update=luaotfload --update=miktex-luatex-base --update=fontspec + # - cmd: mpm.exe --update=miktex-bin-2.9 --verbose + # - cmd: mpm.exe --update=ltxbase --update=luatexbase --update=luaotfload --update=miktex-luatex-base --update=fontspec # Manually install required texlive packages - cmd: mpm.exe --install-some texlive_packages.txt diff --git a/texlive_packages.txt b/texlive_packages.txt index 7a3e79cb..f16d7e2a 100644 --- a/texlive_packages.txt +++ b/texlive_packages.txt @@ -1,11 +1,7 @@ -babel-english babel-esperanto -babel-french -babel-german babel-italian babel-latin babel-portuges -babel-spanish ctablestack etoolbox fancybox From 6cb869c38cf3e35e5eb6fd2688581f5b0a55e314 Mon Sep 17 00:00:00 2001 From: Louis Date: Thu, 28 Jan 2016 22:18:10 +0100 Subject: [PATCH 23/31] AppVeyor fix --- test/test_patatools/test_convert.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_patatools/test_convert.py b/test/test_patatools/test_convert.py index 5fa479b3..0c84adf1 100644 --- a/test/test_patatools/test_convert.py +++ b/test/test_patatools/test_convert.py @@ -54,7 +54,7 @@ class TestConvert(unittest.TestCase, metaclass=dynamic.DynamicTest): with open_read(destname) as destfile: self.assertMultiLineEqual( destfile.read().strip(), - expected, + expected.replace(r'\r\n', r'\n').strip(), ) def assertFailConvert(self, basename, in_format, out_format): # pylint: disable=invalid-name From 75741ce9c0a9a4662dee0b48a816cdc82ef77556 Mon Sep 17 00:00:00 2001 From: Louis Date: Fri, 29 Jan 2016 07:56:32 +0100 Subject: [PATCH 24/31] Fix AppVeyor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Again… --- test/test_patatools/test_convert.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_patatools/test_convert.py b/test/test_patatools/test_convert.py index 0c84adf1..1c9bf237 100644 --- a/test/test_patatools/test_convert.py +++ b/test/test_patatools/test_convert.py @@ -53,8 +53,8 @@ class TestConvert(unittest.TestCase, metaclass=dynamic.DynamicTest): ) with open_read(destname) as destfile: self.assertMultiLineEqual( - destfile.read().strip(), - expected.replace(r'\r\n', r'\n').strip(), + destfile.read().replace(r'\r\n', r'\n').strip(), + expected.strip(), ) def assertFailConvert(self, basename, in_format, out_format): # pylint: disable=invalid-name From 713241a3c1f9ed4e084c1562cf5a4c4a0770835c Mon Sep 17 00:00:00 2001 From: Oliverpool Date: Fri, 29 Jan 2016 09:34:41 +0100 Subject: [PATCH 25/31] Prevent ResourceWarning --- patacrep/songs/__init__.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/patacrep/songs/__init__.py b/patacrep/songs/__init__.py index 3745ab9e..3a7a92d0 100644 --- a/patacrep/songs/__init__.py +++ b/patacrep/songs/__init__.py @@ -194,11 +194,12 @@ class Song: # https://bugs.python.org/issue1692335 return cached = {attr: getattr(self, attr) for attr in self.cached_attributes} - pickle.dump( - cached, - open(self.cached_name, 'wb'), - protocol=-1 - ) + with open(self.cached_name, 'wb') as cache_file: + pickle.dump( + cached, + cache_file, + protocol=-1 + ) def __str__(self): return str(self.fullpath) From e4c10b3eec564ddf0982384cc3fe0321ec9a8df7 Mon Sep 17 00:00:00 2001 From: Oliverpool Date: Fri, 29 Jan 2016 09:34:58 +0100 Subject: [PATCH 26/31] AppVeyor fix attempt --- test/test_patatools/test_convert.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_patatools/test_convert.py b/test/test_patatools/test_convert.py index 1c9bf237..cccc3f66 100644 --- a/test/test_patatools/test_convert.py +++ b/test/test_patatools/test_convert.py @@ -54,7 +54,7 @@ class TestConvert(unittest.TestCase, metaclass=dynamic.DynamicTest): with open_read(destname) as destfile: self.assertMultiLineEqual( destfile.read().replace(r'\r\n', r'\n').strip(), - expected.strip(), + expected.replace(r'\r\n', r'\n').strip(), ) def assertFailConvert(self, basename, in_format, out_format): # pylint: disable=invalid-name From 3b8f97118567f475bb3598fa9cf1611d5528c3ea Mon Sep 17 00:00:00 2001 From: Oliverpool Date: Fri, 29 Jan 2016 09:39:56 +0100 Subject: [PATCH 27/31] AppVeyor fix attempt bis --- test/test_patatools/test_convert.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_patatools/test_convert.py b/test/test_patatools/test_convert.py index cccc3f66..f882fe88 100644 --- a/test/test_patatools/test_convert.py +++ b/test/test_patatools/test_convert.py @@ -53,8 +53,8 @@ class TestConvert(unittest.TestCase, metaclass=dynamic.DynamicTest): ) with open_read(destname) as destfile: self.assertMultiLineEqual( - destfile.read().replace(r'\r\n', r'\n').strip(), - expected.replace(r'\r\n', r'\n').strip(), + destfile.read().replace('\r\n', '\n').strip(), + expected.strip(), ) def assertFailConvert(self, basename, in_format, out_format): # pylint: disable=invalid-name From 66332b28a3b4582cf80da15d6de6426defc4c28e Mon Sep 17 00:00:00 2001 From: Louis Date: Sun, 31 Jan 2016 22:29:37 +0100 Subject: [PATCH 28/31] Add `newunicodechar` LaTeX package Partial fix of #133 --- patacrep/data/latex/patacrep.sty | 3 +++ 1 file changed, 3 insertions(+) diff --git a/patacrep/data/latex/patacrep.sty b/patacrep/data/latex/patacrep.sty index dc817ceb..3dc6c7bf 100644 --- a/patacrep/data/latex/patacrep.sty +++ b/patacrep/data/latex/patacrep.sty @@ -78,6 +78,9 @@ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Unicode characters \RequirePackage{fontspec} +\RequirePackage{newunicodechar} + +\newunicodechar{₂}{\ensuremath{_2}} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% From 6702a740db6abde4010e47b2a13989402d554083 Mon Sep 17 00:00:00 2001 From: Louis Date: Tue, 9 Feb 2016 17:22:43 +0100 Subject: [PATCH 29/31] [songbook] Nicer formatted help text --- patacrep/songbook/__main__.py | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/patacrep/songbook/__main__.py b/patacrep/songbook/__main__.py index 3338b099..9de2a520 100644 --- a/patacrep/songbook/__main__.py +++ b/patacrep/songbook/__main__.py @@ -51,6 +51,7 @@ def argument_parser(args): parser = argparse.ArgumentParser( prog="songbook", description="A song book compiler", + formatter_class=argparse.RawTextHelpFormatter, ) parser.add_argument( @@ -65,9 +66,8 @@ def argument_parser(args): parser.add_argument( '--datadir', '-d', nargs='+', type=str, action='append', help=textwrap.dedent("""\ - Data location. Expected (not necessarily required) - subdirectories are 'songs', 'img', 'latex', 'templates'. - """) + Data location. Expected (not necessarily required) subdirectories are 'songs', 'img', 'latex', 'templates'. + """), ) parser.add_argument( @@ -90,20 +90,15 @@ def argument_parser(args): '--steps', '-s', nargs=1, type=str, action=ParseStepsAction, help=textwrap.dedent("""\ - Steps to run. Default is "{steps}". - Available steps are: - "tex" produce .tex file from templates; - "pdf" compile .tex file; - "sbx" compile index files; - "clean" remove temporary files; - any string beginning with '#' (in this case, it will be run - in a shell). Several steps (excepted the custom shell - command) can be combinend in one --steps argument, as a - comma separated string. - - Substring {{basename}} is replaced by the basename of the song - book, and substrings {{aux}}, {{log}}, {{out}}, {{pdf}}, {{sxc}}, {{tex}} - are replaced by ".aux", ".log", and so on. + Steps to run. Default is "{steps}". Available steps are: + - "tex" produce .tex file from templates; + - "pdf" compile .tex file; + - "sbx" compile index files; + - "clean" remove temporary files; + - any string beginning with '#' (in this case, it will be run in a shell). + Several steps (excepted the custom shell command) can be combinend in one --steps argument, as a comma separated string. + + Substring {{basename}} is replaced by the basename of the song book, and substrings {{aux}}, {{log}}, {{out}}, {{pdf}}, {{sxc}}, {{tex}} are replaced by ".aux", ".log", and so on. """.format(steps=','.join(DEFAULT_STEPS))), default=None, ) From 829989364aeaece2fa6a36716571c01d11f7a322 Mon Sep 17 00:00:00 2001 From: Louis Date: Tue, 9 Feb 2016 17:42:13 +0100 Subject: [PATCH 30/31] [songbook] Add option --error Closes #123 --- patacrep/build.py | 5 +++++ patacrep/content/song.py | 18 +++++++++++++++++- patacrep/errors.py | 10 +++++----- patacrep/songbook/__main__.py | 20 ++++++++++++++++++++ 4 files changed, 47 insertions(+), 6 deletions(-) diff --git a/patacrep/build.py b/patacrep/build.py index bf1e2918..f0b5f7d5 100644 --- a/patacrep/build.py +++ b/patacrep/build.py @@ -114,7 +114,12 @@ class Songbook: self._config['filename'] = output.name[:-4] renderer.render_tex(output, self._config) + + # Get all errors, and maybe exit program self._errors.extend(renderer.errors) + if self.config['_error'] == "failonbook": + if self.has_errors(): + raise errors.SongbookError("Some songs contain errors. Stopping as requested.") def has_errors(self): """Return `True` iff errors have been encountered in the book. diff --git a/patacrep/content/song.py b/patacrep/content/song.py index 8e9519dc..71a399ba 100755 --- a/patacrep/content/song.py +++ b/patacrep/content/song.py @@ -24,6 +24,12 @@ class SongRenderer(ContentItem): """Iterate over song errors.""" yield from self.song.errors + def has_errors(self): + """Return `True` iff errors has been found.""" + for _ in self.iter_errors(): + return True + return False + def begin_new_block(self, previous, __context): """Return a boolean stating if a new block is to be created.""" return not isinstance(previous, SongRenderer) @@ -57,7 +63,7 @@ class SongRenderer(ContentItem): """Order by song path""" return self.song.fullpath < other.song.fullpath -#pylint: disable=unused-argument +#pylint: disable=unused-argument, too-many-branches def parse(keyword, argument, contentlist, config): """Parse data associated with keyword 'song'. @@ -107,7 +113,17 @@ def parse(keyword, argument, contentlist, config): )) except ContentError as error: songlist.append_error(error) + if config['_error'] == "failonsong": + raise errors.SongbookError( + "Error in song '{}'. Stopping as requested." + .format(os.path.join(songdir.fullpath, filename)) + ) continue + if renderer.has_errors() and config['_error'] == "failonsong": + raise errors.SongbookError( + "Error in song '{}'. Stopping as requested." + .format(os.path.join(songdir.fullpath, filename)) + ) songlist.append(renderer) config["_langs"].add(renderer.song.lang) if len(songlist) > before: diff --git a/patacrep/errors.py b/patacrep/errors.py index c111ca3b..19c92087 100644 --- a/patacrep/errors.py +++ b/patacrep/errors.py @@ -5,11 +5,6 @@ class SongbookError(Exception): Songbook errors should inherit from this one. """ - pass - -class YAMLError(SongbookError): - """Error during songbook file decoding""" - def __init__(self, message=None): super().__init__() self.message = message @@ -17,6 +12,11 @@ class YAMLError(SongbookError): def __str__(self): return self.message + +class YAMLError(SongbookError): + """Error during songbook file decoding""" + pass + class TemplateError(SongbookError): """Error during template generation""" diff --git a/patacrep/songbook/__main__.py b/patacrep/songbook/__main__.py index 9de2a520..33cabee7 100644 --- a/patacrep/songbook/__main__.py +++ b/patacrep/songbook/__main__.py @@ -86,6 +86,25 @@ def argument_parser(args): default=[True], ) + parser.add_argument( + '--error', '-e', nargs=1, + help=textwrap.dedent("""\ + By default, this program tries hard to fix or ignore song and book errors. This option changes this behaviour: + - failonsong: stop as soon as a song contains (at least) one error; + - failonbook: stop when all the songs have been parsed and rendered, if any error was met; + - fix: tries to fix (or ignore) errors. + + Note that compilation *may* fail even with `--error=fix`. + """), + type=str, + choices=[ + "failonsong", + "failonbook", + "fix", + ], + default=["fix"], + ) + parser.add_argument( '--steps', '-s', nargs=1, type=str, action=ParseStepsAction, @@ -127,6 +146,7 @@ def main(args): for datadir in reversed(options.datadir): songbook['datadir'].insert(0, datadir) songbook['_cache'] = options.cache[0] + songbook['_error'] = options.error[0] sb_builder = SongbookBuilder(songbook) sb_builder.unsafe = True From 932e39e6a891cae2e110457261def793edca78eb Mon Sep 17 00:00:00 2001 From: Louis Date: Wed, 10 Feb 2016 11:03:14 +0100 Subject: [PATCH 31/31] [songbook] `main()` now accepts zero arguments --- patacrep/songbook/__main__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/patacrep/songbook/__main__.py b/patacrep/songbook/__main__.py index 33cabee7..416b7077 100644 --- a/patacrep/songbook/__main__.py +++ b/patacrep/songbook/__main__.py @@ -127,8 +127,11 @@ def argument_parser(args): return options -def main(args): +def main(args=None): """Main function:""" + if args is None: + args = sys.argv + # set script locale to match user's try: locale.setlocale(locale.LC_ALL, '') @@ -166,4 +169,4 @@ def main(args): sys.exit(0) if __name__ == '__main__': - main(sys.argv) + main()