Browse Source

Merge branch 'yaml_options' into yaml

pull/194/merge
Oliverpool 9 years ago
parent
commit
9bcd5f869f
  1. 39
      examples/example-all.yaml.sb
  2. 566
      patacrep/Rx.py
  3. 61
      patacrep/authors.py
  4. 61
      patacrep/build.py
  5. 8
      patacrep/content/include.py
  6. 4
      patacrep/content/tex.py
  7. 53
      patacrep/data/templates/default.tex
  8. 2
      patacrep/data/templates/layout.tex
  9. 115
      patacrep/data/templates/patacrep.tex
  10. 121
      patacrep/data/templates/songbook_model.yml
  11. 56
      patacrep/data/templates/songs.tex
  12. 3
      patacrep/index.py
  13. 31
      patacrep/songbook/__main__.py
  14. 21
      patacrep/songs/__init__.py
  15. 1
      patacrep/songs/convert/__main__.py
  16. 96
      patacrep/templates.py
  17. 19
      patacrep/utils.py
  18. 2
      test/test_authors.py
  19. 8
      test/test_content/test_content.py
  20. 14
      test/test_song/test_parser.py
  21. 15
      test/test_songbook/content.sb
  22. 7
      test/test_songbook/content.tex.control
  23. 12
      test/test_songbook/datadir.sb
  24. 7
      test/test_songbook/datadir.tex.control
  25. 5
      test/test_songbook/languages.sb
  26. 8
      test/test_songbook/languages.tex.control
  27. 7
      test/test_songbook/syntax.sb
  28. 8
      test/test_songbook/syntax.tex.control
  29. 7
      test/test_songbook/unicode.sb
  30. 8
      test/test_songbook/unicode.tex.control

39
examples/example-all.yaml.sb

@ -1,17 +1,26 @@
bookoptions: book:
- "diagram" lang: fr
- "repeatchords" encoding: utf8
- "lilypond" template: patacrep.tex
- "pictures" datadir: "."
booktype: "chorded" pictures: yes
datadir: "." chords:
template: "patacrep.tex" show: yes
lang: "fr" diagramreminder: all
encoding: "utf8" repeatchords: yes
authwords:
sep: authors:
separators:
- "and" - "and"
- "et" - "et"
content: content: [["sorted"]]
-
- "sorted" template:
patacrep.tex:
color:
songlink: FF0000
hyperlink: 0000FF
bgcolor:
note: D1E4AE
songnumber: AED1E4
index: E4AED1 #not enough songs to see it

566
patacrep/Rx.py

@ -0,0 +1,566 @@
# Downloaded from https://github.com/rjbs/rx
# The contents of the Rx repository are copyright (C) 2008, Ricardo SIGNES.
# They may be distributed under the terms of the
# GNU Public License (GPL) Version 2, June 1991
#pylint: skip-file
import re
import types
from numbers import Number
core_types = [ ]
class SchemaError(Exception):
pass
class SchemaMismatch(Exception):
pass
class SchemaTypeMismatch(SchemaMismatch):
def __init__(self, name, desired_type):
SchemaMismatch.__init__(self, '{0} must be {1}'.format(name, desired_type))
class SchemaValueMismatch(SchemaMismatch):
def __init__(self, name, value):
SchemaMismatch.__init__(self, '{0} must equal {1}'.format(name, value))
class SchemaRangeMismatch(SchemaMismatch):
pass
def indent(text, level=1, whitespace=' '):
return '\n'.join(whitespace*level+line for line in text.split('\n'))
class Util(object):
@staticmethod
def make_range_check(opt):
if not {'min', 'max', 'min-ex', 'max-ex'}.issuperset(opt):
raise ValueError("illegal argument to make_range_check")
if {'min', 'min-ex'}.issubset(opt):
raise ValueError("Cannot define both exclusive and inclusive min")
if {'max', 'max-ex'}.issubset(opt):
raise ValueError("Cannot define both exclusive and inclusive max")
r = opt.copy()
inf = float('inf')
def check_range(value):
return(
r.get('min', -inf) <= value and \
r.get('max', inf) >= value and \
r.get('min-ex', -inf) < value and \
r.get('max-ex', inf) > value
)
return check_range
@staticmethod
def make_range_validator(opt):
check_range = Util.make_range_check(opt)
r = opt.copy()
nan = float('nan')
def validate_range(value, name='value'):
if not check_range(value):
if r.get('min', nan) == r.get('max', nan):
msg = '{0} must equal {1}'.format(name, r['min'])
raise SchemaRangeMismatch(msg)
range_str = ''
if 'min' in r:
range_str = '[{0}, '.format(r['min'])
elif 'min-ex' in r:
range_str = '({0}, '.format(r['min-ex'])
else:
range_str = '(-inf, '
if 'max' in r:
range_str += '{0}]'.format(r['max'])
elif 'max-ex' in r:
range_str += '{0})'.format(r['max-ex'])
else:
range_str += 'inf)'
raise SchemaRangeMismatch(name+' must be in range '+range_str)
return validate_range
class Factory(object):
def __init__(self, register_core_types=True):
self.prefix_registry = {
'': 'tag:codesimply.com,2008:rx/core/',
'.meta': 'tag:codesimply.com,2008:rx/meta/',
}
self.type_registry = {}
if register_core_types:
for t in core_types: self.register_type(t)
@staticmethod
def _default_prefixes(): pass
def expand_uri(self, type_name):
if re.match('^\w+:', type_name): return type_name
m = re.match('^/([-._a-z0-9]*)/([-._a-z0-9]+)$', type_name)
if not m:
raise ValueError("couldn't understand type name '{0}'".format(type_name))
prefix, suffix = m.groups()
if prefix not in self.prefix_registry:
raise KeyError(
"unknown prefix '{0}' in type name '{1}'".format(prefix, type_name)
)
return self.prefix_registry[ prefix ] + suffix
def add_prefix(self, name, base):
if self.prefix_registry.get(name):
raise SchemaError("the prefix '{0}' is already registered".format(name))
self.prefix_registry[name] = base;
def register_type(self, t):
t_uri = t.uri()
if t_uri in self.type_registry:
raise ValueError("type already registered for {0}".format(t_uri))
self.type_registry[t_uri] = t
def learn_type(self, uri, schema):
if self.type_registry.get(uri):
raise SchemaError("tried to learn type for already-registered uri {0}".format(uri))
# make sure schema is valid
# should this be in a try/except?
self.make_schema(schema)
self.type_registry[uri] = { 'schema': schema }
def make_schema(self, schema):
if isinstance(schema, str):
schema = { 'type': schema }
if not isinstance(schema, dict):
raise SchemaError('invalid schema argument to make_schema')
uri = self.expand_uri(schema['type'])
if not self.type_registry.get(uri): raise SchemaError("unknown type {0}".format(uri))
type_class = self.type_registry[uri]
if isinstance(type_class, dict):
if not {'type'}.issuperset(schema):
raise SchemaError('composed type does not take check arguments');
return self.make_schema(type_class['schema'])
else:
return type_class(schema, self)
class _CoreType(object):
@classmethod
def uri(self):
return 'tag:codesimply.com,2008:rx/core/' + self.subname()
def __init__(self, schema, rx):
if not {'type'}.issuperset(schema):
raise SchemaError('unknown parameter for //{0}'.format(self.subname()))
def check(self, value):
try:
self.validate(value)
except SchemaMismatch:
return False
return True
def validate(self, value, name='value'):
raise SchemaMismatch('Tried to validate abstract base schema class')
class AllType(_CoreType):
@staticmethod
def subname(): return 'all'
def __init__(self, schema, rx):
if not {'type', 'of'}.issuperset(schema):
raise SchemaError('unknown parameter for //all')
if not(schema.get('of') and len(schema.get('of'))):
raise SchemaError('no alternatives given in //all of')
self.alts = [rx.make_schema(s) for s in schema['of']]
def validate(self, value, name='value'):
error_messages = []
for schema in self.alts:
try:
schema.validate(value, name)
except SchemaMismatch as e:
error_messages.append(str(e))
if len(error_messages) > 1:
messages = indent('\n'.join(error_messages))
message = '{0} failed to meet all schema requirements:\n{1}'
message = message.format(name, messages)
raise SchemaMismatch(message)
elif len(error_messages) == 1:
raise SchemaMismatch(error_messages[0])
class AnyType(_CoreType):
@staticmethod
def subname(): return 'any'
def __init__(self, schema, rx):
self.alts = None
if not {'type', 'of'}.issuperset(schema):
raise SchemaError('unknown parameter for //any')
if 'of' in schema:
if not schema['of']: raise SchemaError('no alternatives given in //any of')
self.alts = [ rx.make_schema(alt) for alt in schema['of'] ]
def validate(self, value, name='value'):
if self.alts is None:
return
error_messages = []
for schema in self.alts:
try:
schema.validate(value, name)
break
except SchemaMismatch as e:
error_messages.append(str(e))
if len(error_messages) == len(self.alts):
messages = indent('\n'.join(error_messages))
message = '{0} failed to meet any schema requirements:\n{1}'
message = message.format(name, messages)
raise SchemaMismatch(message)
class ArrType(_CoreType):
@staticmethod
def subname(): return 'arr'
def __init__(self, schema, rx):
self.length = None
if not {'type', 'contents', 'length'}.issuperset(schema):
raise SchemaError('unknown parameter for //arr')
if not schema.get('contents'):
raise SchemaError('no contents provided for //arr')
self.content_schema = rx.make_schema(schema['contents'])
if schema.get('length'):
self.length = Util.make_range_validator(schema['length'])
def validate(self, value, name='value'):
if not isinstance(value, (list, tuple)):
raise SchemaTypeMismatch(name, 'array')
if self.length:
self.length(len(value), name+' length')
error_messages = []
for i, item in enumerate(value):
try:
self.content_schema.validate(item, 'item '+str(i))
except SchemaMismatch as e:
error_messages.append(str(e))
if len(error_messages) > 1:
messages = indent('\n'.join(error_messages))
message = '{0} sequence contains invalid elements:\n{1}'
message = message.format(name, messages)
raise SchemaMismatch(message)
elif len(error_messages) == 1:
raise SchemaMismatch(name+': '+error_messages[0])
class BoolType(_CoreType):
@staticmethod
def subname(): return 'bool'
def validate(self, value, name='value'):
if not isinstance(value, bool):
raise SchemaTypeMismatch(name, 'boolean')
class DefType(_CoreType):
@staticmethod
def subname(): return 'def'
def validate(self, value, name='value'):
if value is None:
raise SchemaMismatch(name+' must be non-null')
class FailType(_CoreType):
@staticmethod
def subname(): return 'fail'
def check(self, value): return False
def validate(self, value, name='value'):
raise SchemaMismatch(name+' is of fail type, automatically invalid.')
class IntType(_CoreType):
@staticmethod
def subname(): return 'int'
def __init__(self, schema, rx):
if not {'type', 'range', 'value'}.issuperset(schema):
raise SchemaError('unknown parameter for //int')
self.value = None
if 'value' in schema:
if not isinstance(schema['value'], Number) or schema['value'] % 1 != 0:
raise SchemaError('invalid value parameter for //int')
self.value = schema['value']
self.range = None
if 'range' in schema:
self.range = Util.make_range_validator(schema['range'])
def validate(self, value, name='value'):
if not isinstance(value, Number) or isinstance(value, bool) or value%1:
raise SchemaTypeMismatch(name,'integer')
if self.range:
self.range(value, name)
if self.value is not None and value != self.value:
raise SchemaValueMismatch(name, self.value)
class MapType(_CoreType):
@staticmethod
def subname(): return 'map'
def __init__(self, schema, rx):
self.allowed = set()
if not {'type', 'values'}.issuperset(schema):
raise SchemaError('unknown parameter for //map')
if not schema.get('values'):
raise SchemaError('no values given for //map')
self.value_schema = rx.make_schema(schema['values'])
def validate(self, value, name='value'):
if not isinstance(value, dict):
raise SchemaTypeMismatch(name, 'map')
error_messages = []
for key, val in value.items():
try:
self.value_schema.validate(val, key)
except SchemaMismatch as e:
error_messages.append(str(e))
if len(error_messages) > 1:
messages = indent('\n'.join(error_messages))
message = '{0} map contains invalid entries:\n{1}'
message = message.format(name, messages)
raise SchemaMismatch(message)
elif len(error_messages) == 1:
raise SchemaMismatch(name+': '+error_messages[0])
class NilType(_CoreType):
@staticmethod
def subname(): return 'nil'
def check(self, value): return value is None
def validate(self, value, name='value'):
if value is not None:
raise SchemaTypeMismatch(name, 'null')
class NumType(_CoreType):
@staticmethod
def subname(): return 'num'
def __init__(self, schema, rx):
if not {'type', 'range', 'value'}.issuperset(schema):
raise SchemaError('unknown parameter for //num')
self.value = None
if 'value' in schema:
if not isinstance(schema['value'], Number):
raise SchemaError('invalid value parameter for //num')
self.value = schema['value']
self.range = None
if schema.get('range'):
self.range = Util.make_range_validator(schema['range'])
def validate(self, value, name='value'):
if not isinstance(value, Number) or isinstance(value, bool):
raise SchemaTypeMismatch(name, 'number')
if self.range:
self.range(value, name)
if self.value is not None and value != self.value:
raise SchemaValueMismatch(name, self.value)
class OneType(_CoreType):
@staticmethod
def subname(): return 'one'
def validate(self, value, name='value'):
if not isinstance(value, (Number, str)):
raise SchemaTypeMismatch(name, 'number or string')
class RecType(_CoreType):
@staticmethod
def subname(): return 'rec'
def __init__(self, schema, rx):
if not {'type', 'rest', 'required', 'optional'}.issuperset(schema):
raise SchemaError('unknown parameter for //rec')
self.known = set()
self.rest_schema = None
if schema.get('rest'): self.rest_schema = rx.make_schema(schema['rest'])
for which in ('required', 'optional'):
setattr(self, which, {})
for field in schema.get(which, {}).keys():
if field in self.known:
raise SchemaError('%s appears in both required and optional' % field)
self.known.add(field)
self.__getattribute__(which)[field] = rx.make_schema(
schema[which][field]
)
def validate(self, value, name='value'):
if not isinstance(value, dict):
raise SchemaTypeMismatch(name, 'record')
unknown = [k for k in value.keys() if k not in self.known]
if unknown and not self.rest_schema:
fields = indent('\n'.join(unknown))
raise SchemaMismatch(name+' contains unknown fields:\n'+fields)
error_messages = []
for field in self.required:
try:
if field not in value:
raise SchemaMismatch('missing required field: '+field)
self.required[field].validate(value[field], field)
except SchemaMismatch as e:
error_messages.append(str(e))
for field in self.optional:
if field not in value: continue
try:
self.optional[field].validate(value[field], field)
except SchemaMismatch as e:
error_messages.append(str(e))
if unknown:
rest = {key: value[key] for key in unknown}
try:
self.rest_schema.validate(rest, name)
except SchemaMismatch as e:
error_messages.append(str(e))
if len(error_messages) > 1:
messages = indent('\n'.join(error_messages))
message = '{0} record is invalid:\n{1}'
message = message.format(name, messages)
raise SchemaMismatch(message)
elif len(error_messages) == 1:
raise SchemaMismatch(name+': '+error_messages[0])
class SeqType(_CoreType):
@staticmethod
def subname(): return 'seq'
def __init__(self, schema, rx):
if not {'type', 'contents', 'tail'}.issuperset(schema):
raise SchemaError('unknown parameter for //seq')
if not schema.get('contents'):
raise SchemaError('no contents provided for //seq')
self.content_schema = [ rx.make_schema(s) for s in schema['contents'] ]
self.tail_schema = None
if (schema.get('tail')):
self.tail_schema = rx.make_schema(schema['tail'])
def validate(self, value, name='value'):
if not isinstance(value, (list, tuple)):
raise SchemaTypeMismatch(name, 'sequence')
if len(value) < len(self.content_schema):
raise SchemaMismatch(name+' is less than expected length')
if len(value) > len(self.content_schema) and not self.tail_schema:
raise SchemaMismatch(name+' exceeds expected length')
error_messages = []
for i, (schema, item) in enumerate(zip(self.content_schema, value)):
try:
schema.validate(item, 'item '+str(i))
except SchemaMismatch as e:
error_messages.append(str(e))
if len(error_messages) > 1:
messages = indent('\n'.join(error_messages))
message = '{0} sequence is invalid:\n{1}'
message = message.format(name, messages)
raise SchemaMismatch(message)
elif len(error_messages) == 1:
raise SchemaMismatch(name+': '+error_messages[0])
if len(value) > len(self.content_schema):
self.tail_schema.validate(value[len(self.content_schema):], name)
class StrType(_CoreType):
@staticmethod
def subname(): return 'str'
def __init__(self, schema, rx):
if not {'type', 'value', 'length'}.issuperset(schema):
raise SchemaError('unknown parameter for //str')
self.value = None
if 'value' in schema:
if not isinstance(schema['value'], str):
raise SchemaError('invalid value parameter for //str')
self.value = schema['value']
self.length = None
if 'length' in schema:
self.length = Util.make_range_validator(schema['length'])
def validate(self, value, name='value'):
if not isinstance(value, str):
raise SchemaTypeMismatch(name, 'string')
if self.value is not None and value != self.value:
raise SchemaValueMismatch(name, '"{0}"'.format(self.value))
if self.length:
self.length(len(value), name+' length')
core_types = [
AllType, AnyType, ArrType, BoolType, DefType,
FailType, IntType, MapType, NilType, NumType,
OneType, RecType, SeqType, StrType
]

61
patacrep/authors.py

@ -5,11 +5,6 @@ import re
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
DEFAULT_AUTHWORDS = {
"after": ["by"],
"ignore": ["unknown"],
"sep": ["and"],
}
RE_AFTER = r"^.*\b{}\b(.*)$" RE_AFTER = r"^.*\b{}\b(.*)$"
RE_SEPARATOR = r"^(.*)\b *{} *(\b.*)?$" RE_SEPARATOR = r"^(.*)\b *{} *(\b.*)?$"
@ -18,23 +13,17 @@ def compile_authwords(authwords):
This regexp will later be used to match these words in authors strings. This regexp will later be used to match these words in authors strings.
""" """
# Fill missing values return {
for (key, value) in DEFAULT_AUTHWORDS.items(): 'ignore': authwords.get('ignore', []),
if key not in authwords: 'after': [
authwords[key] = value re.compile(RE_AFTER.format(word), re.LOCALE)
for word in authwords['after']
# Compilation ],
authwords['after'] = [ 'separators': [
re.compile(RE_AFTER.format(word), re.LOCALE) re.compile(RE_SEPARATOR.format(word), re.LOCALE)
for word in authwords['after'] for word in ([" %s" % word for word in authwords['separators']] + [',', ';'])
] ],
authwords['sep'] = [ }
re.compile(RE_SEPARATOR.format(word), re.LOCALE)
for word in ([" %s" % word for word in authwords['sep']] + [',', ';'])
]
return authwords
def split_author_names(string): def split_author_names(string):
r"""Split author between first and last name. r"""Split author between first and last name.
@ -60,12 +49,12 @@ def split_author_names(string):
return (chunks[-1].strip(), " ".join(chunks[:-1]).strip()) return (chunks[-1].strip(), " ".join(chunks[:-1]).strip())
def split_sep_author(string, sep): def split_sep_author(string, separators):
"""Split authors string according to separators. """Split authors string according to separators.
Arguments: Arguments:
- string: string containing authors names ; - string: string containing authors names ;
- sep: regexp matching a separator. - separators: regexp matching a separator.
>>> split_sep_author("Tintin and Milou", re.compile(RE_SEPARATOR.format("and"))) >>> split_sep_author("Tintin and Milou", re.compile(RE_SEPARATOR.format("and")))
['Tintin', 'Milou'] ['Tintin', 'Milou']
@ -73,12 +62,12 @@ def split_sep_author(string, sep):
['Tintin'] ['Tintin']
""" """
authors = [] authors = []
match = sep.match(string) match = separators.match(string)
while match: while match:
if match.group(2) is not None: if match.group(2) is not None:
authors.append(match.group(2).strip()) authors.append(match.group(2).strip())
string = match.group(1) string = match.group(1)
match = sep.match(string) match = separators.match(string)
authors.insert(0, string.strip()) authors.insert(0, string.strip())
return authors return authors
@ -105,7 +94,7 @@ def processauthors_removeparen(authors_string):
dest += char dest += char
return dest return dest
def processauthors_split_string(authors_string, sep): def processauthors_split_string(authors_string, separators):
"""Split strings """Split strings
See docstring of processauthors() for more information. See docstring of processauthors() for more information.
@ -121,7 +110,7 @@ def processauthors_split_string(authors_string, sep):
['Tintin', 'Milou'] ['Tintin', 'Milou']
""" """
authors_list = [authors_string] authors_list = [authors_string]
for sepword in sep: for sepword in separators:
dest = [] dest = []
for author in authors_list: for author in authors_list:
dest.extend(split_sep_author(author, sepword)) dest.extend(split_sep_author(author, sepword))
@ -171,7 +160,7 @@ def processauthors_clean_authors(authors_list):
if author.lstrip() if author.lstrip()
] ]
def processauthors(authors_string, after=None, ignore=None, sep=None): def processauthors(authors_string, after=None, ignore=None, separators=None):
r"""Return an iterator of authors r"""Return an iterator of authors
For example, in the following call: For example, in the following call:
@ -186,7 +175,7 @@ def processauthors(authors_string, after=None, ignore=None, sep=None):
... **compile_authwords({ ... **compile_authwords({
... 'after': ["by"], ... 'after': ["by"],
... 'ignore': ["anonymous"], ... 'ignore': ["anonymous"],
... 'sep': ["and", ","], ... 'separators': ["and", ","],
... }) ... })
... )) == {("Blake", "William"), ("Parry", "Hubert"), ("Royal~Choir~of~FooBar", "The")} ... )) == {("Blake", "William"), ("Parry", "Hubert"), ("Royal~Choir~of~FooBar", "The")}
True True
@ -198,7 +187,7 @@ def processauthors(authors_string, after=None, ignore=None, sep=None):
# "Lyrics by William Blake, music by Hubert Parry, # "Lyrics by William Blake, music by Hubert Parry,
and sung by The Royal~Choir~of~FooBar" and sung by The Royal~Choir~of~FooBar"
2) String is split, separators being comma and words from "sep". 2) String is split, separators being comma and words from "separators".
# ["Lyrics by William Blake", "music by Hubert Parry", # ["Lyrics by William Blake", "music by Hubert Parry",
"sung by The Royal~Choir~of~FooBar"] "sung by The Royal~Choir~of~FooBar"]
@ -216,8 +205,8 @@ def processauthors(authors_string, after=None, ignore=None, sep=None):
# ] # ]
""" """
if not sep: if not separators:
sep = [] separators = []
if not after: if not after:
after = [] after = []
if not ignore: if not ignore:
@ -230,17 +219,17 @@ def processauthors(authors_string, after=None, ignore=None, sep=None):
processauthors_removeparen( processauthors_removeparen(
authors_string authors_string
), ),
sep), separators),
after), after),
ignore) ignore)
): ):
yield split_author_names(author) yield split_author_names(author)
def process_listauthors(authors_list, after=None, ignore=None, sep=None): def process_listauthors(authors_list, after=None, ignore=None, separators=None):
"""Process a list of authors, and return the list of resulting authors.""" """Process a list of authors, and return the list of resulting authors."""
authors = [] authors = []
for sublist in [ for sublist in [
processauthors(string, after, ignore, sep) processauthors(string, after, ignore, separators)
for string in authors_list for string in authors_list
]: ]:
authors.extend(sublist) authors.extend(sublist)

61
patacrep/build.py

@ -8,10 +8,12 @@ import threading
import os.path import os.path
from subprocess import Popen, PIPE, call, check_call from subprocess import Popen, PIPE, call, check_call
from patacrep import authors, content, errors, files import yaml
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 from patacrep.templates import TexBookRenderer, iter_bookoptions
from patacrep.songs import DataSubpath, DEFAULT_CONFIG from patacrep.songs import DataSubpath
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
EOL = "\n" EOL = "\n"
@ -40,8 +42,11 @@ class Songbook:
""" """
def __init__(self, raw_songbook, basename): def __init__(self, raw_songbook, basename):
# Validate config
schema = config_model('schema')
utils.validate_yaml_schema(raw_songbook, schema)
self._raw_config = raw_songbook self._raw_config = raw_songbook
self.config = raw_songbook
self.basename = basename self.basename = basename
self._errors = list() self._errors = list()
self._config = dict() self._config = dict()
@ -50,14 +55,8 @@ class Songbook:
def _set_datadir(self): def _set_datadir(self):
"""Set the default values for datadir""" """Set the default values for datadir"""
try:
if isinstance(self._raw_config['datadir'], str):
self._raw_config['datadir'] = [self._raw_config['datadir']]
except KeyError: # No datadir in the raw_songbook
self._raw_config['datadir'] = [os.path.abspath('.')]
abs_datadir = [] abs_datadir = []
for path in self._raw_config['datadir']: for path in self._raw_config['_datadir']:
if os.path.exists(path) and os.path.isdir(path): if os.path.exists(path) and os.path.isdir(path):
abs_datadir.append(os.path.abspath(path)) abs_datadir.append(os.path.abspath(path))
else: else:
@ -65,10 +64,10 @@ class Songbook:
"Ignoring non-existent datadir '{}'.".format(path) "Ignoring non-existent datadir '{}'.".format(path)
) )
self._raw_config['datadir'] = abs_datadir self._raw_config['_datadir'] = abs_datadir
self._raw_config['_songdir'] = [ self._raw_config['_songdir'] = [
DataSubpath(path, 'songs') DataSubpath(path, 'songs')
for path in self._raw_config['datadir'] for path in self._raw_config['_datadir']
] ]
def write_tex(self, output): def write_tex(self, output):
@ -78,29 +77,27 @@ class Songbook:
- output: a file object, in which the file will be written. - output: a file object, in which the file will be written.
""" """
# Updating configuration # Updating configuration
self._config = DEFAULT_CONFIG.copy() self._config = self._raw_config.copy()
self._config.update(self._raw_config)
renderer = TexBookRenderer( renderer = TexBookRenderer(
self._config['template'], self._config['book']['template'],
self._config['datadir'], self._config['_datadir'],
self._config['lang'], self._config['book']['lang'],
self._config['encoding'], self._config['book']['encoding'],
) )
self._config.update(renderer.get_variables()) self._config['_template'] = renderer.get_all_variables(self._config.get('template', {}))
self._config.update(self._raw_config)
self._config['_compiled_authwords'] = authors.compile_authwords( self._config['_compiled_authwords'] = authors.compile_authwords(
copy.deepcopy(self._config['authwords']) copy.deepcopy(self._config['authors'])
) )
# Loading custom plugins # Loading custom plugins
self._config['_content_plugins'] = files.load_plugins( self._config['_content_plugins'] = files.load_plugins(
datadirs=self._config.get('datadir', []), datadirs=self._config['_datadir'],
root_modules=['content'], root_modules=['content'],
keyword='CONTENT_PLUGINS', keyword='CONTENT_PLUGINS',
) )
self._config['_song_plugins'] = files.load_plugins( self._config['_song_plugins'] = files.load_plugins(
datadirs=self._config.get('datadir', []), datadirs=self._config['_datadir'],
root_modules=['songs'], root_modules=['songs'],
keyword='SONG_RENDERERS', keyword='SONG_RENDERERS',
)['tsg'] )['tsg']
@ -113,6 +110,8 @@ class Songbook:
) )
self._config['filename'] = output.name[:-4] self._config['filename'] = output.name[:-4]
self._config['bookoptions'] = iter_bookoptions(self._config)
renderer.render_tex(output, self._config) renderer.render_tex(output, self._config)
self._errors.extend(renderer.errors) self._errors.extend(renderer.errors)
@ -149,7 +148,7 @@ class Songbook:
def requires_lilypond(self): def requires_lilypond(self):
"""Tell if lilypond is part of the bookoptions""" """Tell if lilypond is part of the bookoptions"""
return 'lilypond' in self.config.get('bookoptions', []) return 'lilypond' in iter_bookoptions(self._config)
def _log_pipe(pipe): def _log_pipe(pipe):
"""Log content from `pipe`.""" """Log content from `pipe`."""
@ -360,3 +359,15 @@ class SongbookBuilder:
os.unlink(self.basename + ext) os.unlink(self.basename + ext)
except Exception as exception: except Exception as exception:
raise errors.CleaningError(self.basename + ext, exception) raise errors.CleaningError(self.basename + ext, exception)
def config_model(*args):
"""Get the model structure with schema and default options"""
model_path = pkg_datapath('templates', 'songbook_model.yml')
with encoding.open_read(model_path) as model_file:
data = yaml.load(model_file)
while data and args:
name, *args = args
data = data.get(name)
return data

8
patacrep/content/include.py

@ -41,7 +41,7 @@ def parse(keyword, config, argument, contentlist):
for path in contentlist: for path in contentlist:
try: try:
filepath = load_from_datadirs(path, config.get('datadir', [])) filepath = load_from_datadirs(path, config['_datadir'])
except ContentError as error: except ContentError as error:
new_contentlist.append_error(error) new_contentlist.append_error(error)
continue continue
@ -49,7 +49,7 @@ def parse(keyword, config, argument, contentlist):
try: try:
with encoding.open_read( with encoding.open_read(
filepath, filepath,
encoding=config['encoding'] encoding=config['book']['encoding']
) as content_file: ) as content_file:
new_content = json.load(content_file) new_content = json.load(content_file)
except Exception as error: # pylint: disable=broad-except except Exception as error: # pylint: disable=broad-except
@ -59,9 +59,9 @@ def parse(keyword, config, argument, contentlist):
)) ))
continue continue
config["datadir"].append(os.path.abspath(os.path.dirname(filepath))) config['_datadir'].append(os.path.abspath(os.path.dirname(filepath)))
new_contentlist.extend(process_content(new_content, config)) new_contentlist.extend(process_content(new_content, config))
config["datadir"].pop() config['_datadir'].pop()
return new_contentlist return new_contentlist

4
patacrep/content/tex.py

@ -38,8 +38,8 @@ def parse(keyword, argument, contentlist, config):
filelist = ContentList() filelist = ContentList()
basefolders = itertools.chain( basefolders = itertools.chain(
(path.fullpath for path in config['_songdir']), (path.fullpath for path in config['_songdir']),
files.iter_datadirs(config['datadir']), files.iter_datadirs(config['_datadir']),
files.iter_datadirs(config['datadir'], 'latex'), files.iter_datadirs(config['_datadir'], 'latex'),
) )
for filename in contentlist: for filename in contentlist:
checked_file = None checked_file = None

53
patacrep/data/templates/default.tex

@ -19,34 +19,33 @@
%!- https://github.com/patacrep/ %!- https://github.com/patacrep/
(* variables *) (* variables *)
{ schema:
"classoptions": {"description": {"en": "LaTeX class options", "fr": "Options de la classe LaTeX"}, type: //rec
"type": "flag", required:
"join": ",", title:
"mandatory": true, type: //str
"default": {"default":[]} author:
}, type: //str
"title": {"description": {"en": "Title", "fr": "Titre"}, optional:
"default": {"en": "Guitar songbook", "fr": "Recueil de chansons pour guitare"}, classoptions:
"mandatory":true type: //arr
}, contents: //str
"author": {"description": {"en": "Author", "fr": "Auteur"}, default:
"default": {"en": "The Patacrep Team", "fr": "L'équipe Patacrep"}, title: "Guitar songbook"
"mandatory":true author: "The Patacrep Team"
}, description.en:
"notenamesout": {"description": {"en": "Note names. Can be 'solfedge' (Do, Re, Mi...) or 'alphascale' (A, B, C...).", title: "Title"
"fr": "Nom des notes : 'solfedge' (Do, Ré, Mi...) ou 'alphascale' (A, B, C...)."}, author: "Author"
"default": {"default": "alphascale", "fr": "solfedge"} classoptions: "LaTeX class options"
}
}
(* endvariables -*) (* endvariables -*)
(*- extends "songs.tex" -*) (*- extends "songs.tex" -*)
(*- set indexes = "titleidx,authidx" -*) (*- set indexes = "titleidx,authidx" -*)
(*- set template_var = _template["default.tex"] -*)
(* block documentclass *) (* block documentclass *)
\documentclass[ \documentclass[
(* for option in classoptions *) (* for option in template_var.classoptions *)
((option)), ((option)),
(* endfor *) (* endfor *)
]{article} ]{article}
@ -58,8 +57,8 @@
\usepackage{chords} \usepackage{chords}
\title{((title))} \title{(( template_var.title ))}
\author{((author))} \author{(( template_var.author ))}
\newindex{titleidx}{((filename))_title} \newindex{titleidx}{((filename))_title}
\newauthorindex{authidx}{((filename))_auth} \newauthorindex{authidx}{((filename))_auth}
@ -67,17 +66,17 @@
(* for prefix in titleprefixwords -*) (* for prefix in titleprefixwords -*)
\titleprefixword{((prefix))} \titleprefixword{((prefix))}
(* endfor*) (* endfor*)
(* for word in authwords.ignore -*) (* for word in authors.ignore -*)
\authignoreword{((word))} \authignoreword{((word))}
(* endfor *) (* endfor *)
(* for word in authwords.after -*) (* for word in authors.after -*)
\authbyword{((word))} \authbyword{((word))}
(* endfor *) (* endfor *)
(* for word in authwords.sep -*) (* for word in authors.separators -*)
\authsepword{((word))} \authsepword{((word))}
(* endfor *) (* endfor *)
(* if notenamesout=="alphascale" -*) (* if chords.notation=="alphascale" -*)
\notenamesout{A}{B}{C}{D}{E}{F}{G} \notenamesout{A}{B}{C}{D}{E}{F}{G}
(* else -*) (* else -*)
\notenamesout{La}{Si}{Do}{R\'e}{Mi}{Fa}{Sol} \notenamesout{La}{Si}{Do}{R\'e}{Mi}{Fa}{Sol}

2
patacrep/data/templates/layout.tex

@ -27,7 +27,7 @@
\makeatletter \makeatletter
\def\input@path{ % \def\input@path{ %
(* for dir in datadir|iter_datadirs *) (* for dir in _datadir|iter_datadirs *)
{(( dir | path2posix ))/latex/} % {(( dir | path2posix ))/latex/} %
(* endfor *) (* endfor *)
} }

115
patacrep/data/templates/patacrep.tex

@ -19,60 +19,51 @@
%!- https://github.com/patacrep/ %!- https://github.com/patacrep/
(* variables *) (* variables *)
{ schema:
"subtitle": {"description": {"en": "Subtitle", "fr": "Sous-titre"}, type: //rec
"default": {"default": ""} optional:
}, version: //str
"version":{ "description": {"en": "Version", "fr": "Version"}, required:
"default": {"default": "undefined"} subtitle: //str
}, url: //str
"web": {"description": {"en": "Website", "fr": "Site web"}, email: //str
"default": {"default": "http://www.patacrep.com"} picture: //str
}, picturecopyright: //str
"mail": {"description": {"en": "Email", "fr": "Adresse électronique"}, footer: //str
"default": {"default": "crep@team-on-fire.com"} color:
}, type: //rec
"picture": {"description": {"en": "Cover picture", "fr": "Image de couverture"}, required:
"type": "file", songlink: //str
"default": {"default": "img/treble_a"} hyperlink: //str
}, bgcolor:
"picturecopyright": {"description": {"en": "Copyright for the cover picture", "fr": "Copyright pour l'image de couverture"}, type: //rec
"default": {"default": "Dbolton \\url{http://commons.wikimedia.org/wiki/User:Dbolton}"} required:
}, songnumber: //str
"footer": {"description": {"en": "Footer", "fr": "Pied de page"}, note: //str
"default": {"default": "Generated using Songbook (\\url{http://www.patacrep.com})"} index: //str
},
"songnumberbgcolor": {"description": {"en": "Number Shade", "fr": "Couleur des numéros"}, default:
"type": "color", subtitle: ""
"default": {"default": "D1E4AE"} version: ""
}, url: http://www.patacrep.com"
"notebgcolor": {"description": {"en": "Note Shade", "fr": "Couleur des notes"}, email: crep@team-on-fire.com
"type": "color", picture: img/treble_a
"default": {"default": "D1E4AE"} picturecopyright: "Dbolton \\url{http://commons.wikimedia.org/wiki/User:Dbolton}"
}, footer: "Generated using Songbook (\\url{http://www.patacrep.com})"
"indexbgcolor": {"description": {"en": "Index Shade", "fr": "Couleur d'index"}, color:
"type": "color", songlink: 4e9a06
"default": {"default": "D1E4AE"} hyperlink: 204a87
}, bgcolor:
"titleprefixwords": {"description": {"en": "Ignore some words in the beginning of song titles", songnumber: D1E4AE
"fr": "Ignore des mots dans le classement des chansons"}, note: D1E4AE
"default": {"default": ["The", "Le", "La", "L'", "A", "Au", "Ces", "De", index: D1E4AE
"Des", "El", "Les", "Ma", "Mon", "Un"]}
}
}
(* endvariables -*) (* endvariables -*)
(*- extends "default.tex" -*) (*- extends "default.tex" -*)
(* block songbookpackages *) (* block songbookpackages *)
%! booktype, bookoptions and instruments are defined in "songs.tex"
\usepackage[ \usepackage[
((booktype)), (* for option in bookoptions *)((option)),
(* for option in bookoptions *)
((option)),
(* endfor *)
(* for instrument in instruments *)
((instrument)),
(* endfor *) (* endfor *)
]{crepbook} ]{crepbook}
(* endblock *) (* endblock *)
@ -91,16 +82,18 @@
\pagestyle{empty} \pagestyle{empty}
\definecolor{SongNumberBgColor}{HTML}{((songnumberbgcolor))} (*- set template_var = _template["patacrep.tex"] -*)
\definecolor{NoteBgColor}{HTML}{((notebgcolor))}
\definecolor{IndexBgColor}{HTML}{((indexbgcolor))} \definecolor{SongNumberBgColor}{HTML}{(( template_var.bgcolor.songnumber ))}
\definecolor{NoteBgColor}{HTML}{(( template_var.bgcolor.note ))}
\definecolor{IndexBgColor}{HTML}{(( template_var.bgcolor.index ))}
\renewcommand{\snumbgcolor}{SongNumberBgColor} \renewcommand{\snumbgcolor}{SongNumberBgColor}
\renewcommand{\notebgcolor}{NoteBgColor} \renewcommand{\notebgcolor}{NoteBgColor}
\renewcommand{\idxbgcolor}{IndexBgColor} \renewcommand{\idxbgcolor}{IndexBgColor}
\definecolor{tango-green-3}{HTML}{4e9a06} \definecolor{tango-green-3}{HTML}{(( template_var.color.songlink ))}
\definecolor{tango-blue-3}{HTML}{204a87} \definecolor{tango-blue-3}{HTML}{(( template_var.color.hyperlink ))}
\usepackage[ \usepackage[
bookmarks, bookmarks,
bookmarksopen, bookmarksopen,
@ -111,13 +104,13 @@
]{hyperref} ]{hyperref}
\subtitle{((subtitle))} \subtitle{(( template_var.subtitle ))}
(* if version!="undefined" -*) (* if template_var.version -*)
\version{((version))} \version{(( template_var.version ))}
(* endif *) (* endif *)
\mail{((mail))} \mail{(( template_var.email ))}
\web{((web))} \web{(( template_var.url ))}
\picture{((picture))} \picture{(( template_var.picture ))}
\picturecopyright{((picturecopyright))} \picturecopyright{(( template_var.picturecopyright ))}
\footer{((footer))} \footer{(( template_var.footer ))}
(* endblock *) (* endblock *)

121
patacrep/data/templates/songbook_model.yml

@ -0,0 +1,121 @@
schema:
type: //rec
optional:
content: //any
template: //any
required:
_cache: //bool
_datadir:
type: //arr
contents: //str
book:
type: //rec
required:
encoding: //str
lang: //str
pictures: //bool
template: //str
onesongperpage: //bool
chords:
type: //rec
required:
show: //bool
diagrampage: //bool
repeatchords: //bool
lilypond: //bool
tablatures: //bool
diagramreminder:
type: //any
of:
- type: //str
value: "none"
- type: //str
value: "important"
- type: //str
value: "all"
instrument:
type: //any
of:
- type: //str
value: "guitar"
- type: //str
value: "ukulele"
notation:
type: //any
of:
- type: //str
value: "alphascale"
- type: //str
value: "solfedge"
authors:
type: //rec
required:
separators:
type: //any
of:
- type: //arr
contents: //str
- type: //nil
ignore:
type: //any
of:
- type: //arr
contents: //str
- type: //nil
after:
type: //any
of:
- type: //arr
contents: //str
- type: //nil
titles:
type: //rec
required:
prefix:
type: //any
of:
- type: //arr
contents: //str
- type: //nil
default:
book:
lang: en
encoding: utf-8
pictures: yes
template: default.tex
onesongperpage: no
chords: # Options relatives aux accords
show: yes
diagramreminder: important
diagrampage: yes
repeatchords: yes
lilypond: no
tablatures: no
instrument: guitar
notation: alphascale
authors: # Comment sont analysés les auteurs
separators:
- and
ignore:
- unknown
after:
- by
titles: # Comment sont analysés les titres
prefix:
- The
- Le
- La
- "L'"
- A
- Au
- Ces
- De
- Des
- El
- Les
- Ma
- Mon
- Un

56
patacrep/data/templates/songs.tex

@ -18,62 +18,12 @@
%!- The latest version of this program can be obtained from %!- The latest version of this program can be obtained from
%!- https://github.com/patacrep/ %!- https://github.com/patacrep/
(* variables *)
{
"instruments": {"description": {"en": "Instruments", "fr": "Instruments"},
"type": "flag",
"values": {"guitar": {"en": "Guitare", "fr": "Guitare"},
"ukulele": {"en": "Ukulele", "fr": "Ukulele"}
},
"join": ",",
"mandatory": true,
"default": {"default":["guitar"]}
},
"bookoptions": {"description": {"en": "Options", "fr": "Options"},
"type": "flag",
"values": {"diagram": {"en": "Chords diagrams", "fr": "Diagrammes d'accords"},
"importantdiagramonly": {"en": "Only importants diagrames", "fr": "Diagrammes importants uniquement"},
"lilypond": {"en": "Lilypond music sheets", "fr": "Partitions lilypond"},
"pictures": {"en": "Cover pictures", "fr": "Couvertures d'albums"},
"tabs": {"en": "Tablatures", "fr": "Tablatures"},
"repeatchords": {"en": "Repeat chords", "fr": "Répéter les accords"},
"onesongperpage": {"en": "One song per page", "fr": "Une chanson par page"}
},
"join": ",",
"mandatory": true,
"default": {"default":["diagram","pictures"]}
},
"booktype": {"description": {"en": "Type", "fr": "Type"},
"type": "enum",
"values": {"chorded": {"en": "With guitar chords", "fr": "Avec accords de guitare" },
"lyric": {"en": "Lyrics only", "fr": "Paroles uniquement"}
},
"default": {"default":"chorded"},
"mandatory": true
},
"lang": {"description": {"en": "Language", "fr": "Langue"},
"default": {"en": "en", "fr": "fr"}
},
"titleprefixwords": {"description": {"en": "Ignore some words in the beginning of song titles",
"fr": "Ignore des mots dans le classement des chansons"},
"default": {"default": []}
},
"authwords": {"description": {"en": "Set of options to process author string (LaTeX commands authsepword, authignoreword, authbyword)",
"fr": "Options pour traiter les noms d'auteurs (commandes LaTeX authsepword, authignoreword, authbyword)"},
"default": {"default": {}}
}
}
(* endvariables -*)
(*- extends "layout.tex" -*) (*- extends "layout.tex" -*)
(* block songbookpackages *) (* block songbookpackages *)
\usepackage[ \usepackage[
((booktype)),
(* for option in bookoptions *)((option)), (* for option in bookoptions *)((option)),
(* endfor *) (* endfor *)
(* for instrument in instruments *)((instrument)),
(* endfor *)
]{patacrep} ]{patacrep}
(* endblock *) (* endblock *)
@ -83,12 +33,12 @@
(* for lang in _langs|sort -*) (* for lang in _langs|sort -*)
\PassOptionsToPackage{(( lang | lang2babel ))}{babel} \PassOptionsToPackage{(( lang | lang2babel ))}{babel}
(* endfor *) (* endfor *)
\usepackage[(( lang | lang2babel ))]{babel} \usepackage[(( book.lang | lang2babel ))]{babel}
\lang{(( lang | lang2babel ))} \lang{(( book.lang | lang2babel ))}
\usepackage{graphicx} \usepackage{graphicx}
\graphicspath{ % \graphicspath{ %
(* for dir in datadir|iter_datadirs*) (* for dir in _datadir|iter_datadirs*)
{(( dir | path2posix ))/} % {(( dir | path2posix ))/} %
(* endfor *) (* endfor *)
} }

3
patacrep/index.py

@ -77,6 +77,9 @@ class Index:
def add_keyword(self, key, word): def add_keyword(self, key, word):
"""Add 'word' to self.keywords[key].""" """Add 'word' to self.keywords[key]."""
# Because LaTeX uses 'sep'
if key == 'sep':
key = 'separators'
if key not in self.keywords: if key not in self.keywords:
self.keywords[key] = [] self.keywords[key] = []
self.keywords[key].append(word) self.keywords[key].append(word)

31
patacrep/songbook/__main__.py

@ -8,8 +8,8 @@ import textwrap
import sys import sys
import yaml import yaml
from patacrep.build import SongbookBuilder, DEFAULT_STEPS from patacrep.build import SongbookBuilder, DEFAULT_STEPS, config_model
from patacrep.utils import yesno from patacrep.utils import yesno, DictOfDict
from patacrep import __version__ from patacrep import __version__
from patacrep import errors from patacrep import errors
import patacrep.encoding import patacrep.encoding
@ -133,39 +133,46 @@ def main():
basename = os.path.basename(songbook_path)[:-3] basename = os.path.basename(songbook_path)[:-3]
# Load the user songbook config
try: try:
with patacrep.encoding.open_read(songbook_path) as songbook_file: with patacrep.encoding.open_read(songbook_path) as songbook_file:
songbook = yaml.load(songbook_file) user_songbook = yaml.load(songbook_file)
if 'encoding' in songbook: if 'encoding' in user_songbook:
with patacrep.encoding.open_read( with patacrep.encoding.open_read(
songbook_path, songbook_path,
encoding=songbook['encoding'] encoding=user_songbook['encoding']
) as songbook_file: ) as songbook_file:
songbook = yaml.load(songbook_file) user_songbook = yaml.load(songbook_file)
except Exception as error: # pylint: disable=broad-except except Exception as error: # pylint: disable=broad-except
LOGGER.error(error) LOGGER.error(error)
LOGGER.error("Error while loading file '{}'.".format(songbook_path)) LOGGER.error("Error while loading file '{}'.".format(songbook_path))
sys.exit(1) sys.exit(1)
# Merge the default and user configs
default_songbook = DictOfDict(config_model('default'))
default_songbook.update(user_songbook)
songbook = dict(default_songbook)
# Gathering datadirs # Gathering datadirs
datadirs = [] datadirs = []
if options.datadir: if options.datadir:
# Command line options # Command line options
datadirs += [item[0] for item in options.datadir] datadirs += [item[0] for item in options.datadir]
if 'datadir' in songbook: if 'book' in songbook and 'datadir' in songbook['book']:
if isinstance(songbook['datadir'], str): if isinstance(songbook['book']['datadir'], str):
songbook['datadir'] = [songbook['datadir']] songbook['book']['datadir'] = [songbook['book']['datadir']]
datadirs += [ datadirs += [
os.path.join( os.path.join(
os.path.dirname(os.path.abspath(songbook_path)), os.path.dirname(os.path.abspath(songbook_path)),
path path
) )
for path in songbook['datadir'] for path in songbook['book']['datadir']
] ]
del songbook['book']['datadir']
# Default value # Default value
datadirs.append(os.path.dirname(os.path.abspath(songbook_path))) datadirs.append(os.path.dirname(os.path.abspath(songbook_path)))
songbook['_datadir'] = datadirs
songbook['datadir'] = datadirs
songbook['_cache'] = options.cache[0] songbook['_cache'] = options.cache[0]
try: try:

21
patacrep/songs/__init__.py

@ -14,15 +14,6 @@ from patacrep.songs import errors as song_errors
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
DEFAULT_CONFIG = {
'template': "default.tex",
'lang': 'en',
'content': [],
'titleprefixwords': [],
'encoding': None,
'datadir': [],
}
def cached_name(datadir, filename): def cached_name(datadir, filename):
"""Return the filename of the cache version of the file.""" """Return the filename of the cache version of the file."""
fullpath = os.path.abspath(os.path.join(datadir, '.cache', filename)) fullpath = os.path.abspath(os.path.join(datadir, '.cache', filename))
@ -107,7 +98,7 @@ class Song:
def __init__(self, subpath, config=None, *, datadir=None): def __init__(self, subpath, config=None, *, datadir=None):
if config is None: if config is None:
config = DEFAULT_CONFIG.copy() config = {}
if datadir is None: if datadir is None:
self.datadir = "" self.datadir = ""
@ -120,8 +111,8 @@ class Song:
self.fullpath = os.path.join(self.datadir, subpath) self.fullpath = os.path.join(self.datadir, subpath)
self.subpath = subpath self.subpath = subpath
self._filehash = None self._filehash = None
self.encoding = config["encoding"] self.encoding = config['book']["encoding"]
self.lang = config["lang"] self.lang = config['book']["lang"]
self.config = config self.config = config
self.errors = [] self.errors = []
@ -138,7 +129,7 @@ class Song:
self.unprefixed_titles = [ self.unprefixed_titles = [
unprefixed_title( unprefixed_title(
title, title,
config['titleprefixwords'] config['titles']['prefix']
) )
for title for title
in self.titles in self.titles
@ -230,7 +221,7 @@ class Song:
def iter_datadirs(self, *subpath): def iter_datadirs(self, *subpath):
"""Return an iterator of existing datadirs (with an optionnal subpath) """Return an iterator of existing datadirs (with an optionnal subpath)
""" """
yield from files.iter_datadirs(self.config['datadir'], *subpath) yield from files.iter_datadirs(self.config['_datadir'], *subpath)
def search_datadir_file(self, filename, extensions=None, directories=None): def search_datadir_file(self, filename, extensions=None, directories=None):
"""Search for a file name. """Search for a file name.
@ -258,7 +249,7 @@ class Song:
if extensions is None: if extensions is None:
extensions = [''] extensions = ['']
if directories is None: if directories is None:
directories = self.config['datadir'] directories = self.config['_datadir']
songdir = os.path.dirname(self.fullpath) songdir = os.path.dirname(self.fullpath)
for extension in extensions: for extension in extensions:

1
patacrep/songs/convert/__main__.py

@ -33,6 +33,7 @@ if __name__ == "__main__":
dest = sys.argv[2] dest = sys.argv[2]
song_files = sys.argv[3:] song_files = sys.argv[3:]
# todo : what is the datadir argument used for?
renderers = files.load_plugins( renderers = files.load_plugins(
datadirs=DEFAULT_CONFIG.get('datadir', []), datadirs=DEFAULT_CONFIG.get('datadir', []),
root_modules=['songs'], root_modules=['songs'],

96
patacrep/templates.py

@ -2,14 +2,15 @@
import logging import logging
import re import re
import json
import yaml
from jinja2 import Environment, FileSystemLoader, ChoiceLoader, \ from jinja2 import Environment, FileSystemLoader, ChoiceLoader, \
TemplateNotFound, nodes TemplateNotFound, nodes
from jinja2.ext import Extension from jinja2.ext import Extension
from jinja2.meta import find_referenced_templates as find_templates from jinja2.meta import find_referenced_templates as find_templates
from patacrep import errors, files from patacrep import errors, files, utils
from patacrep.latex import lang2babel, UnknownLanguage from patacrep.latex import lang2babel, UnknownLanguage
import patacrep.encoding import patacrep.encoding
@ -160,37 +161,28 @@ class TexBookRenderer(Renderer):
), ),
) )
def get_variables(self): def get_all_variables(self, user_config):
'''Get and return a dictionary with the default values '''
for all the variables Validate template variables (and set defaults when needed)
''' '''
data = self.get_template_variables(self.template) data = self.get_template_variables(self.template)
variables = dict() variables = dict()
for name, param in data.items(): for name, param in data.items():
variables[name] = self._get_default(param) template_config = user_config.get(name, {})
variables[name] = self._get_variables(param, template_config)
return variables return variables
def _get_default(self, parameter): @staticmethod
def _get_variables(parameter, user_config):
'''Get the default value for the parameter, according to the language. '''Get the default value for the parameter, according to the language.
''' '''
default = None schema = parameter.get('schema', {})
try:
default = parameter['default'] data = utils.DictOfDict(parameter.get('default', {}))
except KeyError: data.update(user_config)
return None
utils.validate_yaml_schema(data, schema)
if self.lang in default: return data
variable = default[self.lang]
elif "default" in default:
variable = default["default"]
elif "en" in default:
variable = default["en"]
elif len(default):
variable = default.popitem()[1]
else:
variable = None
return variable
def get_template_variables(self, template, skip=None): def get_template_variables(self, template, skip=None):
"""Parse the template to extract the variables as a dictionary. """Parse the template to extract the variables as a dictionary.
@ -206,16 +198,19 @@ class TexBookRenderer(Renderer):
skip = [] skip = []
variables = {} variables = {}
(current, templates) = self.parse_template(template) (current, templates) = self.parse_template(template)
if current:
variables[template.name] = current
for subtemplate in templates: for subtemplate in templates:
if subtemplate in skip: if subtemplate in skip:
continue continue
subtemplate = self.jinjaenv.get_template(subtemplate)
variables.update( variables.update(
self.get_template_variables( self.get_template_variables(
subtemplate, subtemplate,
skip + templates skip + templates
) )
) )
variables.update(current)
return variables return variables
def parse_template(self, template): def parse_template(self, template):
@ -242,17 +237,17 @@ class TexBookRenderer(Renderer):
if match: if match:
for var in match: for var in match:
try: try:
subvariables.update(json.loads(var)) subvariables.update(yaml.load(var))
except ValueError as exception: except ValueError as exception:
raise errors.TemplateError( raise errors.TemplateError(
exception, exception,
( (
"Error while parsing json in file " "Error while parsing yaml in file "
"{filename}. The json string was:" "{filename}. The yaml string was:"
"\n'''\n{jsonstring}\n'''" "\n'''\n{yamlstring}\n'''"
).format( ).format(
filename=templatename, filename=templatename,
jsonstring=var, yamlstring=var,
) )
) )
@ -267,3 +262,42 @@ class TexBookRenderer(Renderer):
''' '''
output.write(self.template.render(context)) output.write(self.template.render(context))
def transform_options(config, equivalents):
"""
Get the equivalent name of the checked options
"""
for option in config:
if config[option] and option in equivalents:
yield equivalents[option]
def iter_bookoptions(config):
"""
Extract the bookoptions from the config structure
"""
if config['chords']['show']:
yield 'chorded'
else:
yield 'lyrics'
book_equivalents = {
'pictures': 'pictures',
'onesongperpage': 'onesongperpage',
}
yield from transform_options(config['book'], book_equivalents)
chords_equivalents = {
'lilypond': 'lilypond',
'tablatures': 'tabs',
'repeatchords': 'repeatchords',
}
yield from transform_options(config['chords'], chords_equivalents)
if config['chords']['show']:
if config['chords']['diagramreminder'] == "important":
yield 'importantdiagramonly'
elif config['chords']['diagramreminder'] == "all":
yield 'diagram'
yield config['chords']['instrument']

19
patacrep/utils.py

@ -2,6 +2,8 @@
from collections import UserDict from collections import UserDict
from patacrep import errors, Rx
class DictOfDict(UserDict): class DictOfDict(UserDict):
"""Dictionary, with a recursive :meth:`update` method. """Dictionary, with a recursive :meth:`update` method.
@ -75,3 +77,20 @@ def yesno(string):
string, string,
", ".join(["'{}'".format(string) for string in yes_strings + no_strings]), ", ".join(["'{}'".format(string) for string in yes_strings + no_strings]),
)) ))
def validate_yaml_schema(data, schema):
"""Check that the data respects the schema
Will raise `SBFileError` if the schema is not respected.
"""
rx_checker = Rx.Factory({"register_core_types": True})
schema = rx_checker.make_schema(schema)
if not isinstance(data, dict):
data = dict(data)
try:
schema.validate(data)
except Rx.SchemaMismatch as exception:
msg = 'Could not parse songbook file:\n' + str(exception)
raise errors.SBFileError(msg)

2
test/test_authors.py

@ -49,7 +49,7 @@ PROCESS_AUTHORS_DATA = [
AUTHWORDS = authors.compile_authwords({ AUTHWORDS = authors.compile_authwords({
"after": ["by"], "after": ["by"],
"ignore": ["anonymous", "Anonyme", "anonyme"], "ignore": ["anonymous", "Anonyme", "anonyme"],
"sep": ['and', 'et'], "separators": ['and', 'et'],
}) })
class TestAutors(unittest.TestCase): class TestAutors(unittest.TestCase):

8
test/test_content/test_content.py

@ -7,9 +7,10 @@ import os
import unittest import unittest
import json import json
from patacrep.songs import DataSubpath, DEFAULT_CONFIG from patacrep.songs import DataSubpath
from patacrep import content, files from patacrep import content, files
from patacrep.content import song, section, songsection, tex from patacrep.content import song, section, songsection, tex
from patacrep.build import config_model
from .. import logging_reduced from .. import logging_reduced
from .. import dynamic # pylint: disable=unused-import from .. import dynamic # pylint: disable=unused-import
@ -95,11 +96,12 @@ class FileTest(unittest.TestCase, metaclass=dynamic.DynamicTest):
def _generate_config(cls): def _generate_config(cls):
"""Generate the config to process the content""" """Generate the config to process the content"""
config = DEFAULT_CONFIG.copy() # Load the default songbook config
config = config_model('default')
datadirpaths = [os.path.join(os.path.dirname(__file__), 'datadir')] datadirpaths = [os.path.join(os.path.dirname(__file__), 'datadir')]
config['datadir'] = datadirpaths config['_datadir'] = datadirpaths
config['_songdir'] = [ config['_songdir'] = [
DataSubpath(path, 'songs') DataSubpath(path, 'songs')

14
test/test_song/test_parser.py

@ -9,8 +9,8 @@ import unittest
from pkg_resources import resource_filename from pkg_resources import resource_filename
from patacrep import files from patacrep import files
from patacrep.songs import DEFAULT_CONFIG
from patacrep.encoding import open_read from patacrep.encoding import open_read
from patacrep.build import config_model
from patacrep.songs import errors from patacrep.songs import errors
from .. import logging_reduced from .. import logging_reduced
@ -75,13 +75,15 @@ class FileTest(unittest.TestCase, metaclass=dynamic.DynamicTest):
def _iter_testmethods(cls): def _iter_testmethods(cls):
"""Iterate over song files to test.""" """Iterate over song files to test."""
# Setting datadir # Setting datadir
cls.config = DEFAULT_CONFIG # Load the default songbook config
if 'datadir' not in cls.config: cls.config = config_model('default')
cls.config['datadir'] = []
cls.config['datadir'].append('datadir') if '_datadir' not in cls.config:
cls.config['_datadir'] = []
cls.config['_datadir'].append('datadir')
cls.song_plugins = files.load_plugins( cls.song_plugins = files.load_plugins(
datadirs=cls.config['datadir'], datadirs=cls.config['_datadir'],
root_modules=['songs'], root_modules=['songs'],
keyword='SONG_RENDERERS', keyword='SONG_RENDERERS',
) )

15
test/test_songbook/content.sb

@ -1,6 +1,12 @@
{ book:
"datadir": ["content_datadir"], pictures: yes
"content": [ datadir: content_datadir
lang: en
chords:
repeatchords: no
diagramreminder: all
content: [
["section", "Test of section"], ["section", "Test of section"],
["sorted"], ["sorted"],
["songsection", "Test of song section"], ["songsection", "Test of song section"],
@ -9,5 +15,4 @@
["tex", "foo.tex"] ["tex", "foo.tex"]
], ],
["include", "include.sbc"] ["include", "include.sbc"]
] ]
}

7
test/test_songbook/content.tex.control

@ -23,9 +23,9 @@
]{article} ]{article}
\usepackage[ \usepackage[
chorded, chorded,
diagram,
pictures, pictures,
diagram,
guitar, guitar,
]{patacrep} ]{patacrep}
@ -63,6 +63,9 @@ guitar,
\newindex{titleidx}{content_title} \newindex{titleidx}{content_title}
\newauthorindex{authidx}{content_auth} \newauthorindex{authidx}{content_auth}
\authignoreword{unknown}
\authbyword{by}
\authsepword{and}
\notenamesout{A}{B}{C}{D}{E}{F}{G} \notenamesout{A}{B}{C}{D}{E}{F}{G}

12
test/test_songbook/datadir.sb

@ -1,6 +1,6 @@
bookoptions: book:
- pictures pictures: yes
datadir: datadir:
- datadir_datadir - datadir_datadir
- datadir_datadir2 - datadir_datadir2
lang: en lang: en

7
test/test_songbook/datadir.tex.control

@ -24,8 +24,10 @@
]{article} ]{article}
\usepackage[ \usepackage[
chorded, chorded,
pictures, pictures,
repeatchords,
importantdiagramonly,
guitar, guitar,
]{patacrep} ]{patacrep}
@ -64,6 +66,9 @@ guitar,
\newindex{titleidx}{datadir_title} \newindex{titleidx}{datadir_title}
\newauthorindex{authidx}{datadir_auth} \newauthorindex{authidx}{datadir_auth}
\authignoreword{unknown}
\authbyword{by}
\authsepword{and}
\notenamesout{A}{B}{C}{D}{E}{F}{G} \notenamesout{A}{B}{C}{D}{E}{F}{G}

5
test/test_songbook/languages.sb

@ -1,2 +1,3 @@
datadir: book:
- languages_datadir datadir:
- languages_datadir

8
test/test_songbook/languages.tex.control

@ -23,9 +23,10 @@
]{article} ]{article}
\usepackage[ \usepackage[
chorded, chorded,
diagram,
pictures, pictures,
repeatchords,
importantdiagramonly,
guitar, guitar,
]{patacrep} ]{patacrep}
@ -65,6 +66,9 @@ guitar,
\newindex{titleidx}{languages_title} \newindex{titleidx}{languages_title}
\newauthorindex{authidx}{languages_auth} \newauthorindex{authidx}{languages_auth}
\authignoreword{unknown}
\authbyword{by}
\authsepword{and}
\notenamesout{A}{B}{C}{D}{E}{F}{G} \notenamesout{A}{B}{C}{D}{E}{F}{G}

7
test/test_songbook/syntax.sb

@ -1,3 +1,4 @@
datadir: book:
- syntax_datadir datadir:
lang: en - syntax_datadir
lang: en

8
test/test_songbook/syntax.tex.control

@ -23,9 +23,10 @@
]{article} ]{article}
\usepackage[ \usepackage[
chorded, chorded,
diagram,
pictures, pictures,
repeatchords,
importantdiagramonly,
guitar, guitar,
]{patacrep} ]{patacrep}
@ -62,6 +63,9 @@ guitar,
\newindex{titleidx}{syntax_title} \newindex{titleidx}{syntax_title}
\newauthorindex{authidx}{syntax_auth} \newauthorindex{authidx}{syntax_auth}
\authignoreword{unknown}
\authbyword{by}
\authsepword{and}
\notenamesout{A}{B}{C}{D}{E}{F}{G} \notenamesout{A}{B}{C}{D}{E}{F}{G}

7
test/test_songbook/unicode.sb

@ -1,3 +1,4 @@
datadir: book:
- unicode_datadir datadir:
lang: en - unicode_datadir
lang: en

8
test/test_songbook/unicode.tex.control

@ -23,9 +23,10 @@
]{article} ]{article}
\usepackage[ \usepackage[
chorded, chorded,
diagram,
pictures, pictures,
repeatchords,
importantdiagramonly,
guitar, guitar,
]{patacrep} ]{patacrep}
@ -62,6 +63,9 @@ guitar,
\newindex{titleidx}{unicode_title} \newindex{titleidx}{unicode_title}
\newauthorindex{authidx}{unicode_auth} \newauthorindex{authidx}{unicode_auth}
\authignoreword{unknown}
\authbyword{by}
\authsepword{and}
\notenamesout{A}{B}{C}{D}{E}{F}{G} \notenamesout{A}{B}{C}{D}{E}{F}{G}

Loading…
Cancel
Save