Browse Source

Merge branch 'yaml_options' into yaml

pull/194/merge
Oliverpool 9 years ago
parent
commit
9bcd5f869f
  1. 37
      examples/example-all.yaml.sb
  2. 566
      patacrep/Rx.py
  3. 55
      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. 94
      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. 13
      test/test_songbook/content.sb
  22. 5
      test/test_songbook/content.tex.control
  23. 4
      test/test_songbook/datadir.sb
  24. 5
      test/test_songbook/datadir.tex.control
  25. 1
      test/test_songbook/languages.sb
  26. 6
      test/test_songbook/languages.tex.control
  27. 1
      test/test_songbook/syntax.sb
  28. 6
      test/test_songbook/syntax.tex.control
  29. 1
      test/test_songbook/unicode.sb
  30. 6
      test/test_songbook/unicode.tex.control

37
examples/example-all.yaml.sb

@ -1,17 +1,26 @@
bookoptions:
- "diagram"
- "repeatchords"
- "lilypond"
- "pictures"
booktype: "chorded"
book:
lang: fr
encoding: utf8
template: patacrep.tex
datadir: "."
template: "patacrep.tex"
lang: "fr"
encoding: "utf8"
authwords:
sep:
pictures: yes
chords:
show: yes
diagramreminder: all
repeatchords: yes
authors:
separators:
- "and"
- "et"
content:
-
- "sorted"
content: [["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
]

55
patacrep/authors.py

@ -5,11 +5,6 @@ import re
LOGGER = logging.getLogger(__name__)
DEFAULT_AUTHWORDS = {
"after": ["by"],
"ignore": ["unknown"],
"sep": ["and"],
}
RE_AFTER = 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.
"""
# Fill missing values
for (key, value) in DEFAULT_AUTHWORDS.items():
if key not in authwords:
authwords[key] = value
# Compilation
authwords['after'] = [
return {
'ignore': authwords.get('ignore', []),
'after': [
re.compile(RE_AFTER.format(word), re.LOCALE)
for word in authwords['after']
]
authwords['sep'] = [
],
'separators': [
re.compile(RE_SEPARATOR.format(word), re.LOCALE)
for word in ([" %s" % word for word in authwords['sep']] + [',', ';'])
]
return authwords
for word in ([" %s" % word for word in authwords['separators']] + [',', ';'])
],
}
def split_author_names(string):
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())
def split_sep_author(string, sep):
def split_sep_author(string, separators):
"""Split authors string according to separators.
Arguments:
- 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")))
['Tintin', 'Milou']
@ -73,12 +62,12 @@ def split_sep_author(string, sep):
['Tintin']
"""
authors = []
match = sep.match(string)
match = separators.match(string)
while match:
if match.group(2) is not None:
authors.append(match.group(2).strip())
string = match.group(1)
match = sep.match(string)
match = separators.match(string)
authors.insert(0, string.strip())
return authors
@ -105,7 +94,7 @@ def processauthors_removeparen(authors_string):
dest += char
return dest
def processauthors_split_string(authors_string, sep):
def processauthors_split_string(authors_string, separators):
"""Split strings
See docstring of processauthors() for more information.
@ -121,7 +110,7 @@ def processauthors_split_string(authors_string, sep):
['Tintin', 'Milou']
"""
authors_list = [authors_string]
for sepword in sep:
for sepword in separators:
dest = []
for author in authors_list:
dest.extend(split_sep_author(author, sepword))
@ -171,7 +160,7 @@ def processauthors_clean_authors(authors_list):
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
For example, in the following call:
@ -186,7 +175,7 @@ def processauthors(authors_string, after=None, ignore=None, sep=None):
... **compile_authwords({
... 'after': ["by"],
... 'ignore': ["anonymous"],
... 'sep': ["and", ","],
... 'separators': ["and", ","],
... })
... )) == {("Blake", "William"), ("Parry", "Hubert"), ("Royal~Choir~of~FooBar", "The")}
True
@ -198,7 +187,7 @@ def processauthors(authors_string, after=None, ignore=None, sep=None):
# "Lyrics by William Blake, music by Hubert Parry,
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",
"sung by The Royal~Choir~of~FooBar"]
@ -216,8 +205,8 @@ def processauthors(authors_string, after=None, ignore=None, sep=None):
# ]
"""
if not sep:
sep = []
if not separators:
separators = []
if not after:
after = []
if not ignore:
@ -230,17 +219,17 @@ def processauthors(authors_string, after=None, ignore=None, sep=None):
processauthors_removeparen(
authors_string
),
sep),
separators),
after),
ignore)
):
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."""
authors = []
for sublist in [
processauthors(string, after, ignore, sep)
processauthors(string, after, ignore, separators)
for string in authors_list
]:
authors.extend(sublist)

61
patacrep/build.py

@ -8,10 +8,12 @@ import threading
import os.path
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.templates import TexBookRenderer
from patacrep.songs import DataSubpath, DEFAULT_CONFIG
from patacrep.templates import TexBookRenderer, iter_bookoptions
from patacrep.songs import DataSubpath
LOGGER = logging.getLogger(__name__)
EOL = "\n"
@ -40,8 +42,11 @@ class Songbook:
"""
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.config = raw_songbook
self.basename = basename
self._errors = list()
self._config = dict()
@ -50,14 +55,8 @@ class Songbook:
def _set_datadir(self):
"""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 = []
for path in self._raw_config['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:
@ -65,10 +64,10 @@ class Songbook:
"Ignoring non-existent datadir '{}'.".format(path)
)
self._raw_config['datadir'] = abs_datadir
self._raw_config['_datadir'] = abs_datadir
self._raw_config['_songdir'] = [
DataSubpath(path, 'songs')
for path in self._raw_config['datadir']
for path in self._raw_config['_datadir']
]
def write_tex(self, output):
@ -78,29 +77,27 @@ class Songbook:
- output: a file object, in which the file will be written.
"""
# Updating configuration
self._config = DEFAULT_CONFIG.copy()
self._config.update(self._raw_config)
self._config = self._raw_config.copy()
renderer = TexBookRenderer(
self._config['template'],
self._config['datadir'],
self._config['lang'],
self._config['encoding'],
self._config['book']['template'],
self._config['_datadir'],
self._config['book']['lang'],
self._config['book']['encoding'],
)
self._config.update(renderer.get_variables())
self._config.update(self._raw_config)
self._config['_template'] = renderer.get_all_variables(self._config.get('template', {}))
self._config['_compiled_authwords'] = authors.compile_authwords(
copy.deepcopy(self._config['authwords'])
copy.deepcopy(self._config['authors'])
)
# Loading custom plugins
self._config['_content_plugins'] = files.load_plugins(
datadirs=self._config.get('datadir', []),
datadirs=self._config['_datadir'],
root_modules=['content'],
keyword='CONTENT_PLUGINS',
)
self._config['_song_plugins'] = files.load_plugins(
datadirs=self._config.get('datadir', []),
datadirs=self._config['_datadir'],
root_modules=['songs'],
keyword='SONG_RENDERERS',
)['tsg']
@ -113,6 +110,8 @@ class Songbook:
)
self._config['filename'] = output.name[:-4]
self._config['bookoptions'] = iter_bookoptions(self._config)
renderer.render_tex(output, self._config)
self._errors.extend(renderer.errors)
@ -149,7 +148,7 @@ class Songbook:
def requires_lilypond(self):
"""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):
"""Log content from `pipe`."""
@ -360,3 +359,15 @@ class SongbookBuilder:
os.unlink(self.basename + ext)
except Exception as 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:
try:
filepath = load_from_datadirs(path, config.get('datadir', []))
filepath = load_from_datadirs(path, config['_datadir'])
except ContentError as error:
new_contentlist.append_error(error)
continue
@ -49,7 +49,7 @@ def parse(keyword, config, argument, contentlist):
try:
with encoding.open_read(
filepath,
encoding=config['encoding']
encoding=config['book']['encoding']
) as content_file:
new_content = json.load(content_file)
except Exception as error: # pylint: disable=broad-except
@ -59,9 +59,9 @@ def parse(keyword, config, argument, contentlist):
))
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))
config["datadir"].pop()
config['_datadir'].pop()
return new_contentlist

4
patacrep/content/tex.py

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

53
patacrep/data/templates/default.tex

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

2
patacrep/data/templates/layout.tex

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

115
patacrep/data/templates/patacrep.tex

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

3
patacrep/index.py

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

31
patacrep/songbook/__main__.py

@ -8,8 +8,8 @@ import textwrap
import sys
import yaml
from patacrep.build import SongbookBuilder, DEFAULT_STEPS
from patacrep.utils import yesno
from patacrep.build import SongbookBuilder, DEFAULT_STEPS, config_model
from patacrep.utils import yesno, DictOfDict
from patacrep import __version__
from patacrep import errors
import patacrep.encoding
@ -133,39 +133,46 @@ def main():
basename = os.path.basename(songbook_path)[:-3]
# Load the user songbook config
try:
with patacrep.encoding.open_read(songbook_path) as songbook_file:
songbook = yaml.load(songbook_file)
if 'encoding' in songbook:
user_songbook = yaml.load(songbook_file)
if 'encoding' in user_songbook:
with patacrep.encoding.open_read(
songbook_path,
encoding=songbook['encoding']
encoding=user_songbook['encoding']
) as songbook_file:
songbook = yaml.load(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)
# Merge the default and user configs
default_songbook = DictOfDict(config_model('default'))
default_songbook.update(user_songbook)
songbook = dict(default_songbook)
# Gathering datadirs
datadirs = []
if options.datadir:
# Command line options
datadirs += [item[0] for item in options.datadir]
if 'datadir' in songbook:
if isinstance(songbook['datadir'], str):
songbook['datadir'] = [songbook['datadir']]
if 'book' in songbook and 'datadir' in songbook['book']:
if isinstance(songbook['book']['datadir'], str):
songbook['book']['datadir'] = [songbook['book']['datadir']]
datadirs += [
os.path.join(
os.path.dirname(os.path.abspath(songbook_path)),
path
)
for path in songbook['datadir']
for path in songbook['book']['datadir']
]
del songbook['book']['datadir']
# Default value
datadirs.append(os.path.dirname(os.path.abspath(songbook_path)))
songbook['datadir'] = datadirs
songbook['_datadir'] = datadirs
songbook['_cache'] = options.cache[0]
try:

21
patacrep/songs/__init__.py

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

1
patacrep/songs/convert/__main__.py

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

94
patacrep/templates.py

@ -2,14 +2,15 @@
import logging
import re
import json
import yaml
from jinja2 import Environment, FileSystemLoader, ChoiceLoader, \
TemplateNotFound, nodes
from jinja2.ext import Extension
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
import patacrep.encoding
@ -160,37 +161,28 @@ class TexBookRenderer(Renderer):
),
)
def get_variables(self):
'''Get and return a dictionary with the default values
for all the variables
def get_all_variables(self, user_config):
'''
Validate template variables (and set defaults when needed)
'''
data = self.get_template_variables(self.template)
variables = dict()
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
def _get_default(self, parameter):
@staticmethod
def _get_variables(parameter, user_config):
'''Get the default value for the parameter, according to the language.
'''
default = None
try:
default = parameter['default']
except KeyError:
return None
if self.lang in default:
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
schema = parameter.get('schema', {})
data = utils.DictOfDict(parameter.get('default', {}))
data.update(user_config)
return variable
utils.validate_yaml_schema(data, schema)
return data
def get_template_variables(self, template, skip=None):
"""Parse the template to extract the variables as a dictionary.
@ -206,16 +198,19 @@ class TexBookRenderer(Renderer):
skip = []
variables = {}
(current, templates) = self.parse_template(template)
if current:
variables[template.name] = current
for subtemplate in templates:
if subtemplate in skip:
continue
subtemplate = self.jinjaenv.get_template(subtemplate)
variables.update(
self.get_template_variables(
subtemplate,
skip + templates
)
)
variables.update(current)
return variables
def parse_template(self, template):
@ -242,17 +237,17 @@ class TexBookRenderer(Renderer):
if match:
for var in match:
try:
subvariables.update(json.loads(var))
subvariables.update(yaml.load(var))
except ValueError as exception:
raise errors.TemplateError(
exception,
(
"Error while parsing json in file "
"{filename}. The json string was:"
"\n'''\n{jsonstring}\n'''"
"Error while parsing yaml in file "
"{filename}. The yaml string was:"
"\n'''\n{yamlstring}\n'''"
).format(
filename=templatename,
jsonstring=var,
yamlstring=var,
)
)
@ -267,3 +262,42 @@ class TexBookRenderer(Renderer):
'''
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 patacrep import errors, Rx
class DictOfDict(UserDict):
"""Dictionary, with a recursive :meth:`update` method.
@ -75,3 +77,20 @@ def yesno(string):
string,
", ".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({
"after": ["by"],
"ignore": ["anonymous", "Anonyme", "anonyme"],
"sep": ['and', 'et'],
"separators": ['and', 'et'],
})
class TestAutors(unittest.TestCase):

8
test/test_content/test_content.py

@ -7,9 +7,10 @@ import os
import unittest
import json
from patacrep.songs import DataSubpath, DEFAULT_CONFIG
from patacrep.songs import DataSubpath
from patacrep import content, files
from patacrep.content import song, section, songsection, tex
from patacrep.build import config_model
from .. import logging_reduced
from .. import dynamic # pylint: disable=unused-import
@ -95,11 +96,12 @@ class FileTest(unittest.TestCase, metaclass=dynamic.DynamicTest):
def _generate_config(cls):
"""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')]
config['datadir'] = datadirpaths
config['_datadir'] = datadirpaths
config['_songdir'] = [
DataSubpath(path, 'songs')

14
test/test_song/test_parser.py

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

13
test/test_songbook/content.sb

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

5
test/test_songbook/content.tex.control

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

4
test/test_songbook/datadir.sb

@ -1,5 +1,5 @@
bookoptions:
- pictures
book:
pictures: yes
datadir:
- datadir_datadir
- datadir_datadir2

5
test/test_songbook/datadir.tex.control

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

1
test/test_songbook/languages.sb

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

6
test/test_songbook/languages.tex.control

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

1
test/test_songbook/syntax.sb

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

6
test/test_songbook/syntax.tex.control

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

1
test/test_songbook/unicode.sb

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

6
test/test_songbook/unicode.tex.control

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

Loading…
Cancel
Save