Browse Source

Manually merge branch 'master' into yaml

pull/193/head
Oliverpool 9 years ago
parent
commit
f77b9dcda4
  1. 4
      .travis.yml
  2. 33
      patacrep/build.py
  3. 17
      patacrep/content/song.py
  4. 3
      patacrep/data/latex/patacrep.sty
  5. 6
      patacrep/data/templates/songbook_model.yml
  6. 7
      patacrep/errors.py
  7. 103
      patacrep/songbook/__init__.py
  8. 136
      patacrep/songbook/__main__.py
  9. 11
      patacrep/songs/__init__.py
  10. 8
      patacrep/songs/chordpro/syntax.py
  11. 0
      patacrep/tools/__init__.py
  12. 114
      patacrep/tools/__main__.py
  13. 0
      patacrep/tools/cache/__init__.py
  14. 73
      patacrep/tools/cache/__main__.py
  15. 0
      patacrep/tools/convert/__init__.py
  16. 41
      patacrep/tools/convert/__main__.py
  17. 6
      patatools
  18. 1
      setup.py
  19. 6
      songbook
  20. 1
      test/test_patatools/.gitignore
  21. 0
      test/test_patatools/__init__.py
  22. 67
      test/test_patatools/test_cache.py
  23. 2
      test/test_patatools/test_cache.yaml
  24. 0
      test/test_patatools/test_cache_datadir/songs/foo.csg
  25. 123
      test/test_patatools/test_convert.py
  26. 122
      test/test_patatools/test_convert_failure/song.csg
  27. 0
      test/test_patatools/test_convert_failure/song.csg.tsg.control
  28. 1
      test/test_patatools/test_convert_success/.gitignore
  29. 45
      test/test_patatools/test_convert_success/greensleeves.csg
  30. 62
      test/test_patatools/test_convert_success/greensleeves.csg.tsg.control
  31. 3
      test/test_song/test_parser.py

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:

33
patacrep/build.py

@ -13,7 +13,6 @@ import yaml
from patacrep import authors, content, encoding, errors, files, pkg_datapath, utils from patacrep import authors, content, encoding, errors, files, pkg_datapath, utils
from patacrep.index import process_sxd from patacrep.index import process_sxd
from patacrep.templates import TexBookRenderer, iter_bookoptions from patacrep.templates import TexBookRenderer, iter_bookoptions
from patacrep.songs import DataSubpath
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
EOL = "\n" EOL = "\n"
@ -55,25 +54,6 @@ class Songbook:
self.basename = basename self.basename = basename
self._errors = list() self._errors = list()
self._config = dict() self._config = dict()
# Some special keys have their value processed.
self._set_datadir()
def _set_datadir(self):
"""Set the default values for datadir"""
abs_datadir = []
for path in self._raw_config['_datadir']:
if os.path.exists(path) and os.path.isdir(path):
abs_datadir.append(os.path.abspath(path))
else:
LOGGER.warning(
"Ignoring non-existent datadir '{}'.".format(path)
)
self._raw_config['_datadir'] = abs_datadir
self._raw_config['_songdir'] = [
DataSubpath(path, 'songs')
for path in self._raw_config['_datadir']
]
def write_tex(self, output): def write_tex(self, output):
"""Build the '.tex' file corresponding to self. """Build the '.tex' file corresponding to self.
@ -124,7 +104,12 @@ class Songbook:
self._config['_bookoptions'] = iter_bookoptions(self._config) self._config['_bookoptions'] = iter_bookoptions(self._config)
renderer.render_tex(output, self._config) renderer.render_tex(output, self._config)
# Get all errors, and maybe exit program
self._errors.extend(renderer.errors) 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): def has_errors(self):
"""Return `True` iff errors have been encountered in the book. """Return `True` iff errors have been encountered in the book.
@ -182,11 +167,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 .yaml 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.

17
patacrep/content/song.py

@ -24,6 +24,12 @@ class SongRenderer(ContentItem):
"""Iterate over song errors.""" """Iterate over song errors."""
yield from self.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): def begin_new_block(self, previous, __context):
"""Return a boolean stating if a new block is to be created.""" """Return a boolean stating if a new block is to be created."""
return not isinstance(previous, SongRenderer) return not isinstance(previous, SongRenderer)
@ -58,6 +64,7 @@ class SongRenderer(ContentItem):
return self.song.fullpath < other.song.fullpath return self.song.fullpath < other.song.fullpath
#pylint: disable=unused-argument #pylint: disable=unused-argument
#pylint: disable=too-many-branches
@validate_parser_argument(""" @validate_parser_argument("""
type: //any type: //any
of: of:
@ -117,7 +124,17 @@ def parse(keyword, argument, config):
)) ))
except ContentError as error: except ContentError as error:
songlist.append_error(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 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) songlist.append(renderer)
config["_langs"].add(renderer.song.lang) config["_langs"].add(renderer.song.lang)
if len(songlist) > before: if len(songlist) > before:

3
patacrep/data/latex/patacrep.sty

@ -78,6 +78,9 @@
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Unicode characters % Unicode characters
\RequirePackage{fontspec} \RequirePackage{fontspec}
\RequirePackage{newunicodechar}
\newunicodechar{₂}{\ensuremath{_2}}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

6
patacrep/data/templates/songbook_model.yml

@ -6,9 +6,14 @@ schema:
required: required:
_cache: //bool _cache: //bool
_filepath: //str _filepath: //str
_basename: //str
_error: //str
_datadir: _datadir:
type: //arr type: //arr
contents: //str contents: //str
_songdir:
type: //arr
contents: //any
book: book:
type: //rec type: //rec
required: required:
@ -80,6 +85,7 @@ schema:
- type: //nil - type: //nil
default: default:
en: en:
_datadir: [] # For test reasons
book: book:
lang: en lang: en
encoding: utf-8 encoding: utf-8

7
patacrep/errors.py

@ -5,14 +5,15 @@ class SongbookError(Exception):
Songbook errors should inherit from this one. Songbook errors should inherit from this one.
""" """
pass def __init__(self, message=None):
super().__init__()
self.message = message
class SchemaError(SongbookError): class SchemaError(SongbookError):
"""Error on the songbook schema""" """Error on the songbook schema"""
def __init__(self, message='', rx_exception=None): def __init__(self, message='', rx_exception=None):
super().__init__() super().__init__(message)
self.message = message
self.rx_exception = rx_exception self.rx_exception = rx_exception
def __str__(self): def __str__(self):

103
patacrep/songbook/__init__.py

@ -0,0 +1,103 @@
"""Raw songbook utilities"""
import logging
import os
import sys
import yaml
from patacrep import encoding
from patacrep.build import config_model
from patacrep.utils import DictOfDict
from patacrep.songs import DataSubpath
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 + ".yaml") and not os.path.exists(filename):
filename += ".yaml"
try:
with patacrep.encoding.open_read(filename) as songbook_file:
user_songbook = yaml.load(songbook_file)
if 'encoding' in user_songbook.get('book', []):
with encoding.open_read(
filename,
encoding=user_songbook['book']['encoding']
) as songbook_file:
user_songbook = yaml.load(songbook_file)
except Exception as error: # pylint: disable=broad-except
raise patacrep.errors.SongbookError(str(error))
songbook = _add_songbook_defaults(user_songbook)
songbook['_filepath'] = filename
songbook['_basename'] = os.path.basename(filename)[:-len(".yaml")]
# Gathering datadirs
songbook['_datadir'] = list(_iter_absolute_datadirs(songbook))
if 'datadir' in songbook['book']:
del songbook['book']['datadir']
songbook['_songdir'] = [
DataSubpath(path, 'songs')
for path in songbook['_datadir']
]
return songbook
def _add_songbook_defaults(user_songbook):
""" Adds the defaults values to the songbook if missing from
the user songbook
Priority:
- User values
- Default values of the user lang (if set)
- Default english values
"""
# Merge the default and user configs
locale_default = config_model('default')
# Initialize with default in english
default_songbook = locale_default.get('en', {})
default_songbook = DictOfDict(default_songbook)
if 'lang' in user_songbook.get('book', []):
# Update default with current lang
lang = user_songbook['book']['lang']
default_songbook.update(locale_default.get(lang, {}))
# Update default with user_songbook
default_songbook.update(user_songbook)
return dict(default_songbook)
def _iter_absolute_datadirs(raw_songbook):
"""Iterate on the absolute datadirs of the raw songbook
Appends the songfile dir at the end
"""
datadir = raw_songbook.get('book', {}).get('datadir')
filepath = raw_songbook['_filepath']
if datadir is None:
datadir = []
elif isinstance(datadir, str):
datadir = [datadir]
basedir = os.path.dirname(os.path.abspath(filepath))
for path in datadir:
abspath = os.path.join(basedir, path)
if os.path.exists(abspath) and os.path.isdir(abspath):
yield abspath
else:
LOGGER.warning(
"Ignoring non-existent datadir '{}'.".format(path)
)
yield basedir

136
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, config_model from patacrep.build import SongbookBuilder, DEFAULT_STEPS
from patacrep.utils import yesno, DictOfDict 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
# Logging configuration # Logging configuration
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
@ -53,6 +51,7 @@ def argument_parser(args):
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
prog="songbook", prog="songbook",
description="A song book compiler", description="A song book compiler",
formatter_class=argparse.RawTextHelpFormatter,
) )
parser.add_argument( parser.add_argument(
@ -67,9 +66,8 @@ def argument_parser(args):
parser.add_argument( parser.add_argument(
'--datadir', '-d', nargs='+', type=str, action='append', '--datadir', '-d', nargs='+', type=str, action='append',
help=textwrap.dedent("""\ help=textwrap.dedent("""\
Data location. Expected (not necessarily required) Data location. Expected (not necessarily required) subdirectories are 'songs', 'img', 'latex', 'templates'.
subdirectories are 'songs', 'img', 'latex', 'templates'. """),
""")
) )
parser.add_argument( parser.add_argument(
@ -88,24 +86,38 @@ def argument_parser(args):
default=[True], 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( parser.add_argument(
'--steps', '-s', nargs=1, type=str, '--steps', '-s', nargs=1, type=str,
action=ParseStepsAction, action=ParseStepsAction,
help=textwrap.dedent("""\ help=textwrap.dedent("""\
Steps to run. Default is "{steps}". Steps to run. Default is "{steps}". Available steps are:
Available steps are: - "tex" produce .tex file from templates;
"tex" produce .tex file from templates; - "pdf" compile .tex file;
"pdf" compile .tex file; - "sbx" compile index files;
"sbx" compile index files; - "clean" remove temporary files;
"clean" remove temporary files; - any string beginning with '#' (in this case, it will be run in a shell).
any string beginning with '#' (in this case, it will be run Several steps (excepted the custom shell command) can be combinend in one --steps argument, as a comma separated string.
in a shell). Several steps (excepted the custom shell
command) can be combinend in one --steps argument, as a Substring {{basename}} is replaced by the basename of the song book, and substrings {{aux}}, {{log}}, {{out}}, {{pdf}}, {{sxc}}, {{tex}} are replaced by "<BASENAME>.aux", "<BASENAME>.log", and so on.
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 "<BASENAME>.aux", "<BASENAME>.log", and so on.
""".format(steps=','.join(DEFAULT_STEPS))), """.format(steps=','.join(DEFAULT_STEPS))),
default=None, default=None,
) )
@ -115,8 +127,10 @@ def argument_parser(args):
return options return options
def main(): def main(args=None):
"""Main function:""" """Main function:"""
if args is None:
args = sys.argv
# set script locale to match user's # set script locale to match user's
try: try:
@ -125,55 +139,22 @@ 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] songbook_path = options.book[-1]
if os.path.exists(songbook_path + ".yaml") and not os.path.exists(songbook_path):
songbook_path += ".yaml"
basename = os.path.basename(songbook_path)[:-len(".yaml")]
# Load the user songbook config # Load the user songbook config
try: try:
with patacrep.encoding.open_read(songbook_path) as songbook_file: songbook = open_songbook(songbook_path)
user_songbook = yaml.load(songbook_file)
if 'encoding' in user_songbook.get('book', []):
with patacrep.encoding.open_read(
songbook_path,
encoding=user_songbook['book']['encoding']
) as songbook_file:
user_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 = add_songbook_defaults(user_songbook)
songbook['_filepath'] = os.path.abspath(songbook_path)
sbdir = os.path.dirname(songbook['_filepath'])
# Gathering datadirs
datadirs = []
if options.datadir:
# Command line options # Command line options
datadirs += [item[0] for item in options.datadir] if options.datadir:
if 'book' in songbook and 'datadir' in songbook['book']: for datadir in reversed(options.datadir):
if isinstance(songbook['book']['datadir'], str): songbook['datadir'].insert(0, datadir)
songbook['book']['datadir'] = [songbook['book']['datadir']] songbook['_cache'] = options.cache[0]
datadirs += [ songbook['_error'] = options.error[0]
os.path.join(sbdir, path)
for path in songbook['book']['datadir']
]
del songbook['book']['datadir']
# Default value
datadirs.append(sbdir)
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)
@ -190,30 +171,5 @@ def main():
sys.exit(0) sys.exit(0)
def add_songbook_defaults(user_songbook):
""" Adds the defaults values to the songbook if missing from
the user songbook
Priority:
- User values
- Default values of the user lang (if set)
- Default english values
"""
# Merge the default and user configs
locale_default = config_model('default')
# Initialize with default in english
default_songbook = locale_default.get('en', {})
default_songbook = DictOfDict(default_songbook)
if 'lang' in user_songbook.get('book', []):
# Update default with current lang
lang = user_songbook['book']['lang']
default_songbook.update(locale_default.get(lang, {}))
# Update default with user_songbook
default_songbook.update(user_songbook)
return dict(default_songbook)
if __name__ == '__main__': if __name__ == '__main__':
main() main()

11
patacrep/songs/__init__.py

@ -185,11 +185,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

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

@ -1,41 +1,44 @@
"""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.songs import DEFAULT_CONFIG from patacrep.content import ContentError
from patacrep.utils import yesno from patacrep.utils import yesno
from patacrep.build import config_model
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:]
# todo : what is the datadir argument used for? # todo : what is the datadir argument used for?
renderers = files.load_plugins( renderers = files.load_plugins(
datadirs=DEFAULT_CONFIG.get('datadir', []), datadirs=[],
root_modules=['songs'], root_modules=['songs'],
keyword='SONG_RENDERERS', keyword='SONG_RENDERERS',
) )
@ -56,8 +59,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, config_model('default')['en'])
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):
@ -65,6 +68,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)
@ -74,3 +80,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.yaml"]),
(cache_main, ["patatools-cache", "clean", "test_cache.yaml"]),
]:
with self.subTest(main=main, args=args):
# First compilation. Ensure that cache exists afterwards
self._system(songbook_main, ["songbook", "--steps", "tex,clean", "test_cache.yaml"])
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.yaml"]),
(cache_main, ["patatools-cache", "clean", "test_cache.yaml"]),
]:
with self.subTest(main=main, args=args):
# Clean cache
self._system(main, args)

2
test/test_patatools/test_cache.yaml

@ -0,0 +1,2 @@
book:
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

Loading…
Cancel
Save