Browse Source

Merge pull request #189 from patacrep/patatools

Create a `patatools` binary, gathering miscellaneous patacrep-related tools.
pull/195/head
Louis 9 years ago
parent
commit
392abfd5fe
  1. 6
      .appveyor.yml
  2. 4
      .travis.yml
  3. 8
      patacrep/build.py
  4. 2
      patacrep/errors.py
  5. 54
      patacrep/songbook/__init__.py
  6. 62
      patacrep/songbook/__main__.py
  7. 11
      patacrep/songs/__init__.py
  8. 8
      patacrep/songs/chordpro/syntax.py
  9. 0
      patacrep/tools/__init__.py
  10. 114
      patacrep/tools/__main__.py
  11. 0
      patacrep/tools/cache/__init__.py
  12. 73
      patacrep/tools/cache/__main__.py
  13. 0
      patacrep/tools/convert/__init__.py
  14. 37
      patacrep/tools/convert/__main__.py
  15. 6
      patatools
  16. 1
      setup.py
  17. 6
      songbook
  18. 1
      test/test_patatools/.gitignore
  19. 0
      test/test_patatools/__init__.py
  20. 67
      test/test_patatools/test_cache.py
  21. 3
      test/test_patatools/test_cache.sb
  22. 0
      test/test_patatools/test_cache_datadir/songs/foo.csg
  23. 123
      test/test_patatools/test_convert.py
  24. 122
      test/test_patatools/test_convert_failure/song.csg
  25. 0
      test/test_patatools/test_convert_failure/song.csg.tsg.control
  26. 1
      test/test_patatools/test_convert_success/.gitignore
  27. 45
      test/test_patatools/test_convert_success/greensleeves.csg
  28. 62
      test/test_patatools/test_convert_success/greensleeves.csg.tsg.control
  29. 3
      test/test_song/test_parser.py
  30. 4
      texlive_packages.txt

6
.appveyor.yml

@ -23,7 +23,7 @@ install:
- "python -c \"import struct; print(struct.calcsize('P') * 8)\"" - "python -c \"import struct; print(struct.calcsize('P') * 8)\""
# Download miktex portable (if not cached) # 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 # Unzip miktex portable
- "7z x miktex-portable.exe * -aot -omiktex > nul" - "7z x miktex-portable.exe * -aot -omiktex > nul"
@ -32,8 +32,8 @@ install:
- cmd: set PATH=%PATH%;C:\projects\patacrep\miktex\miktex\bin - cmd: set PATH=%PATH%;C:\projects\patacrep\miktex\miktex\bin
# Update some packages to prevent ltluatex bug # Update some packages to prevent ltluatex bug
- cmd: mpm.exe --update=miktex-bin-2.9 # - cmd: mpm.exe --update=miktex-bin-2.9 --verbose
- cmd: mpm.exe --update=ltxbase --update=luatexbase --update=luaotfload --update=miktex-luatex-base --update=fontspec # - cmd: mpm.exe --update=ltxbase --update=luatexbase --update=luaotfload --update=miktex-luatex-base --update=fontspec
# Manually install required texlive packages # Manually install required texlive packages
- cmd: mpm.exe --install-some texlive_packages.txt - cmd: mpm.exe --install-some texlive_packages.txt

4
.travis.yml

@ -2,11 +2,11 @@ git:
depth: 1 depth: 1
language: python language: python
python: python:
- 3.4 - 3.5
install: install:
- pip install tox - pip install tox
script: script:
- tox - tox -e lint,py35
sudo: false sudo: false
# addons: # addons:
# apt: # apt:

8
patacrep/build.py

@ -172,11 +172,11 @@ class SongbookBuilder:
# are function; values are return values of functions. # are function; values are return values of functions.
_called_functions = {} _called_functions = {}
def __init__(self, raw_songbook, basename): def __init__(self, raw_songbook):
# Representation of the .sb songbook configuration file.
self.songbook = Songbook(raw_songbook, basename)
# Basename of the songbook to be built. # 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): def _run_once(self, function, *args, **kwargs):
"""Run function if it has not been run yet. """Run function if it has not been run yet.

2
patacrep/errors.py

@ -7,7 +7,7 @@ class SongbookError(Exception):
""" """
pass pass
class SBFileError(SongbookError): class YAMLError(SongbookError):
"""Error during songbook file decoding""" """Error during songbook file decoding"""
def __init__(self, message=None): def __init__(self, message=None):

54
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

62
patacrep/songbook/__main__.py

@ -3,16 +3,14 @@
import argparse import argparse
import locale import locale
import logging import logging
import os.path
import textwrap
import sys 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 __version__
from patacrep import errors from patacrep import errors
import patacrep.encoding from patacrep.songbook import open_songbook
from patacrep.build import SongbookBuilder, DEFAULT_STEPS
from patacrep.utils import yesno
# Logging configuration # Logging configuration
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
@ -115,9 +113,8 @@ def argument_parser(args):
return options return options
def main(): def main(args):
"""Main function:""" """Main function:"""
# set script locale to match user's # set script locale to match user's
try: try:
locale.setlocale(locale.LC_ALL, '') locale.setlocale(locale.LC_ALL, '')
@ -125,51 +122,18 @@ def main():
# Locale is not installed on user's system, or wrongly configured. # Locale is not installed on user's system, or wrongly configured.
LOGGER.error("Locale error: {}\n".format(str(error))) LOGGER.error("Locale error: {}\n".format(str(error)))
options = argument_parser(sys.argv[1:]) options = argument_parser(args[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: try:
with patacrep.encoding.open_read(songbook_path) as songbook_file: songbook = open_songbook(options.book[-1])
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)
# Gathering datadirs
datadirs = []
if options.datadir:
# Command line options # Command line options
datadirs += [item[0] for item in options.datadir] if options.datadir:
if 'datadir' in songbook: for datadir in reversed(options.datadir):
if isinstance(songbook['datadir'], str): songbook['datadir'].insert(0, datadir)
songbook['datadir'] = [songbook['datadir']] songbook['_cache'] = options.cache[0]
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]
try: sb_builder = SongbookBuilder(songbook)
sb_builder = SongbookBuilder(songbook, basename)
sb_builder.unsafe = True sb_builder.unsafe = True
sb_builder.build_steps(options.steps) sb_builder.build_steps(options.steps)
@ -187,4 +151,4 @@ def main():
sys.exit(0) sys.exit(0)
if __name__ == '__main__': if __name__ == '__main__':
main() main(sys.argv)

11
patacrep/songs/__init__.py

@ -194,11 +194,12 @@ class Song:
# https://bugs.python.org/issue1692335 # https://bugs.python.org/issue1692335
return return
cached = {attr: getattr(self, attr) for attr in self.cached_attributes} cached = {attr: getattr(self, attr) for attr in self.cached_attributes}
pickle.dump( with open(self.cached_name, 'wb') as cache_file:
cached, pickle.dump(
open(self.cached_name, 'wb'), cached,
protocol=-1 cache_file,
) protocol=-1
)
def __str__(self): def __str__(self):
return str(self.fullpath) return str(self.fullpath)

8
patacrep/songs/chordpro/syntax.py

@ -38,6 +38,8 @@ class ChordproParser(Parser):
lexer = ChordProLexer(filename=self.filename) lexer = ChordProLexer(filename=self.filename)
ast.AST.lexer = lexer.lexer ast.AST.lexer = lexer.lexer
parsed = self.parser.parse(content, 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) parsed.error_builders.extend(lexer.error_builders)
return parsed return parsed
@ -325,8 +327,4 @@ class ChordproParser(Parser):
def parse_song(content, filename=None): def parse_song(content, filename=None):
"""Parse song and return its metadata.""" """Parse song and return its metadata."""
parser = ChordproParser(filename) return ChordproParser(filename).parse(content)
parsed_content = parser.parse(content)
if parsed_content is None:
raise ContentError(message='Fatal error during song parsing.')
return parsed_content

0
patacrep/tools/__init__.py

114
patacrep/tools/__main__.py

@ -0,0 +1,114 @@
#!/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("patatools")
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<subcommand>[^\.]*)\.__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
<https://docs.python.org/3/library/argparse.html#nargs>`_ `nargs` setting
does not include the remainder arguments if the first one begins with `-`.
This bug is reperted as `17050 <https://bugs.python.org/issue17050>`_. 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.",
)
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)
sub1.add_argument('remainder', nargs=argparse.REMAINDER)
sub1.set_defaults(function=function)
return parser
def main(args):
"""Main function"""
parser = commandline_parser()
args = parser.parse_args(args[1:])
args.function(["patatools-{}".format(args.subcommand)] + args.remainder)
if __name__ == "__main__":
main(sys.argv)

0
patacrep/tools/cache/__init__.py

73
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.songbook import open_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
clean = subparsers.add_parser(
"clean",
description="Delete cache.",
help="Delete cache.",
)
clean.add_argument(
'songbook',
metavar="SONGBOOK",
help=textwrap.dedent("""Songbook file to be used to look for cache path."""),
type=filename,
)
clean.set_defaults(command=do_clean)
return parser
def do_clean(namespace):
"""Execute the `patatools cache clean` command."""
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):
shutil.rmtree(cachedir)
def main(args):
"""Main function: run from command line."""
options = commandline_parser().parse_args(args[1:])
try:
options.command(options)
except errors.SongbookError as error:
LOGGER.error(str(error))
sys.exit(1)
if __name__ == "__main__":
main(sys.argv)

0
patacrep/tools/convert/__init__.py

37
patacrep/songs/convert/__main__.py → patacrep/tools/convert/__main__.py

@ -1,37 +1,40 @@
"""Conversion between formats """`patatools.convert` command: convert between song formats"""
See the :meth:`__usage` method for more information.
"""
import os import os
import logging import logging
import sys import sys
from patacrep import files from patacrep import files
from patacrep.content import ContentError
from patacrep.songs import DEFAULT_CONFIG from patacrep.songs import DEFAULT_CONFIG
from patacrep.utils import yesno from patacrep.utils import yesno
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger("patatools.convert")
SUBCOMMAND_DESCRIPTION = "Convert between song formats"
def __usage(): def _usage():
return "python3 -m patacrep.songs.convert INPUTFORMAT OUTPUTFORMAT FILES" return "patatools convert INPUTFORMAT OUTPUTFORMAT FILES"
def confirm(destname): def confirm(destname):
"""Ask whether destination name should be overwrited."""
while True: while True:
try: try:
return yesno(input("File '{}' already exist. Overwrite? [yn] ".format(destname))) return yesno(input("File '{}' already exist. Overwrite? [yn] ".format(destname)))
except ValueError: except ValueError:
continue continue
if __name__ == "__main__": def main(args=None):
if len(sys.argv) < 4: """Main function: run from command line."""
if args is None:
args = sys.argv
if len(args) < 4:
LOGGER.error("Invalid number of arguments.") LOGGER.error("Invalid number of arguments.")
LOGGER.error("Usage: %s", __usage()) LOGGER.error("Usage: %s", _usage())
sys.exit(1) sys.exit(1)
source = sys.argv[1] source = args[1]
dest = sys.argv[2] dest = args[2]
song_files = sys.argv[3:] song_files = args[3:]
renderers = files.load_plugins( renderers = files.load_plugins(
datadirs=DEFAULT_CONFIG.get('datadir', []), datadirs=DEFAULT_CONFIG.get('datadir', []),
@ -55,8 +58,8 @@ if __name__ == "__main__":
sys.exit(1) sys.exit(1)
for file in song_files: for file in song_files:
song = renderers[dest][source](file, DEFAULT_CONFIG)
try: try:
song = renderers[dest][source](file, DEFAULT_CONFIG)
destname = "{}.{}".format(".".join(file.split(".")[:-1]), dest) destname = "{}.{}".format(".".join(file.split(".")[:-1]), dest)
if os.path.exists(destname): if os.path.exists(destname):
if not confirm(destname): if not confirm(destname):
@ -64,6 +67,9 @@ if __name__ == "__main__":
with open(destname, "w") as destfile: with open(destname, "w") as destfile:
destfile.write(song.render()) destfile.write(song.render())
except ContentError:
LOGGER.error("Cannot parse file '%s'.", file)
sys.exit(1)
except NotImplementedError: except NotImplementedError:
LOGGER.error("Cannot convert to format '%s'.", dest) LOGGER.error("Cannot convert to format '%s'.", dest)
sys.exit(1) sys.exit(1)
@ -73,3 +79,6 @@ if __name__ == "__main__":
sys.exit(0) sys.exit(0)
sys.exit(0) sys.exit(0)
if __name__ == "__main__":
main()

6
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 $@

1
setup.py

@ -39,6 +39,7 @@ setup(
entry_points={ entry_points={
'console_scripts': [ 'console_scripts': [
"songbook = patacrep.songbook.__main__:main", "songbook = patacrep.songbook.__main__:main",
"patatools = patacrep.tools.__main__:main",
], ],
}, },
classifiers=[ classifiers=[

6
songbook

@ -3,8 +3,4 @@
# Do not edit this file. This file is just a helper file for development test. # Do not edit this file. This file is just a helper file for development test.
# It is not part of the distributed software. # It is not part of the distributed software.
if [ "$1" = "purgecache" ]; then python -m patacrep.songbook "$@"
find . -name ".cache" -type d -print -exec rm -r {} +
else
python -m patacrep.songbook "$@"
fi

1
test/test_patatools/.gitignore

@ -0,0 +1 @@
.cache

0
test/test_patatools/__init__.py

67
test/test_patatools/test_cache.py

@ -0,0 +1,67 @@
"""Tests of the patatools-cache command."""
# pylint: disable=too-few-public-methods
import os
import shutil
import unittest
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", ".cache")
class TestCache(unittest.TestCase):
"""Test of the "patatools cache" subcommand"""
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))
@staticmethod
def _remove_cache():
"""Delete cache."""
shutil.rmtree(CACHEDIR, ignore_errors=True)
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"""
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", "--steps", "tex,clean", "test_cache.sb"])
self.assertTrue(os.path.exists(CACHEDIR))
# Clean cache
self._system(main, args)
# 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)

3
test/test_patatools/test_cache.sb

@ -0,0 +1,3 @@
{
"datadir": ["test_cache_datadir"],
}

0
test/test_patatools/test_cache_datadir/songs/foo.csg

123
test/test_patatools/test_convert.py

@ -0,0 +1,123 @@
"""Tests of the patatools-convert command."""
# pylint: disable=too-few-public-methods
import contextlib
import glob
import os
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 .. import dynamic # pylint: disable=unused-import
from .. import logging_reduced
class TestConvert(unittest.TestCase, metaclass=dynamic.DynamicTest):
"""Test of the "patatools convert" subcommand"""
@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): # 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_success"):
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]),
0,
)
expected = controlfile.read().strip().replace(
"@TEST_FOLDER@",
files.path2posix(resource_filename(__name__, "")),
)
with open_read(destname) as destfile:
self.assertMultiLineEqual(
destfile.read().replace('\r\n', '\n').strip(),
expected.strip(),
)
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)
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 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(*pathlist):
"""Temporary change current directory, relative to this file directory"""
with files.chdir(resource_filename(__name__, ""), *pathlist):
yield
@classmethod
def _iter_testmethods(cls):
"""Iterate over song files to test."""
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_success(cls, base, in_format, out_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__ = (
"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 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}'." # pylint: disable=line-too-long
).format(base=os.path.basename(base), in_format=in_format, out_format=out_format)
return test_parse_render

122
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

0
test/test_patatools/test_convert_failure/song.csg.tsg.control

1
test/test_patatools/test_convert_success/.gitignore

@ -0,0 +1 @@
greensleeves.tsg

45
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

62
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

3
test/test_song/test_parser.py

@ -48,8 +48,7 @@ class FileTest(unittest.TestCase, metaclass=dynamic.DynamicTest):
@staticmethod @staticmethod
@contextlib.contextmanager @contextlib.contextmanager
def chdir(*path): 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): with files.chdir(resource_filename(__name__, ""), *path):
yield yield

4
texlive_packages.txt

@ -1,11 +1,7 @@
babel-english
babel-esperanto babel-esperanto
babel-french
babel-german
babel-italian babel-italian
babel-latin babel-latin
babel-portuges babel-portuges
babel-spanish
ctablestack ctablestack
etoolbox etoolbox
fancybox fancybox

Loading…
Cancel
Save