diff --git a/examples/example-all.yaml.sb b/examples/example-all.yaml.sb index 3a167565..01a95015 100644 --- a/examples/example-all.yaml.sb +++ b/examples/example-all.yaml.sb @@ -1,17 +1,26 @@ -bookoptions: - - "diagram" - - "repeatchords" - - "lilypond" - - "pictures" -booktype: "chorded" -datadir: "." -template: "patacrep.tex" -lang: "fr" -encoding: "utf8" -authwords: - sep: +book: + lang: fr + encoding: utf8 + template: patacrep.tex + datadir: "." + pictures: yes +chords: + show: yes + diagramreminder: all + repeatchords: yes + +authors: + separators: - "and" - "et" -content: - - - - "sorted" \ No newline at end of file +content: [["sorted"]] + +template: + patacrep.tex: + color: + songlink: FF0000 + hyperlink: 0000FF + bgcolor: + note: D1E4AE + songnumber: AED1E4 + index: E4AED1 #not enough songs to see it \ No newline at end of file diff --git a/patacrep/Rx.py b/patacrep/Rx.py new file mode 100644 index 00000000..677a3461 --- /dev/null +++ b/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 +] diff --git a/patacrep/authors.py b/patacrep/authors.py index 6ea3098b..c8b65361 100644 --- a/patacrep/authors.py +++ b/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'] = [ - re.compile(RE_AFTER.format(word), re.LOCALE) - for word in authwords['after'] - ] - authwords['sep'] = [ - re.compile(RE_SEPARATOR.format(word), re.LOCALE) - for word in ([" %s" % word for word in authwords['sep']] + [',', ';']) - ] - - return authwords - + return { + 'ignore': authwords.get('ignore', []), + 'after': [ + re.compile(RE_AFTER.format(word), re.LOCALE) + for word in authwords['after'] + ], + 'separators': [ + re.compile(RE_SEPARATOR.format(word), re.LOCALE) + 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) diff --git a/patacrep/build.py b/patacrep/build.py index 2fed6c31..96a09424 100644 --- a/patacrep/build.py +++ b/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 diff --git a/patacrep/content/include.py b/patacrep/content/include.py index 92f060ab..dbc10fce 100644 --- a/patacrep/content/include.py +++ b/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 diff --git a/patacrep/content/tex.py b/patacrep/content/tex.py index 2f20f70b..dd506655 100755 --- a/patacrep/content/tex.py +++ b/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 diff --git a/patacrep/data/templates/default.tex b/patacrep/data/templates/default.tex index a9504965..5fbf5c10 100644 --- a/patacrep/data/templates/default.tex +++ b/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} diff --git a/patacrep/data/templates/layout.tex b/patacrep/data/templates/layout.tex index 63227530..ea048417 100644 --- a/patacrep/data/templates/layout.tex +++ b/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 *) } diff --git a/patacrep/data/templates/patacrep.tex b/patacrep/data/templates/patacrep.tex index b9a01f1e..48cb7bd4 100644 --- a/patacrep/data/templates/patacrep.tex +++ b/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 *) diff --git a/patacrep/data/templates/songbook_model.yml b/patacrep/data/templates/songbook_model.yml new file mode 100644 index 00000000..7567de47 --- /dev/null +++ b/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 diff --git a/patacrep/data/templates/songs.tex b/patacrep/data/templates/songs.tex index 07ed0f33..94846f0d 100644 --- a/patacrep/data/templates/songs.tex +++ b/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 *) } diff --git a/patacrep/index.py b/patacrep/index.py index 767c04ba..d572f872 100644 --- a/patacrep/index.py +++ b/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) diff --git a/patacrep/songbook/__main__.py b/patacrep/songbook/__main__.py index b62dcf73..adc581c5 100644 --- a/patacrep/songbook/__main__.py +++ b/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: diff --git a/patacrep/songs/__init__.py b/patacrep/songs/__init__.py index 3745ab9e..adf473c5 100644 --- a/patacrep/songs/__init__.py +++ b/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: diff --git a/patacrep/songs/convert/__main__.py b/patacrep/songs/convert/__main__.py index 19d7041e..1c2e8cfb 100644 --- a/patacrep/songs/convert/__main__.py +++ b/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'], diff --git a/patacrep/templates.py b/patacrep/templates.py index 9194cf7f..f745a58e 100644 --- a/patacrep/templates.py +++ b/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 - - return variable + schema = parameter.get('schema', {}) + + data = utils.DictOfDict(parameter.get('default', {})) + data.update(user_config) + + 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'] diff --git a/patacrep/utils.py b/patacrep/utils.py index 998fe738..7ddd0eca 100644 --- a/patacrep/utils.py +++ b/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) diff --git a/test/test_authors.py b/test/test_authors.py index 8e560102..659e0993 100644 --- a/test/test_authors.py +++ b/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): diff --git a/test/test_content/test_content.py b/test/test_content/test_content.py index 5857506c..23d498bc 100644 --- a/test/test_content/test_content.py +++ b/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') diff --git a/test/test_song/test_parser.py b/test/test_song/test_parser.py index 298f0017..5dacf81d 100644 --- a/test/test_song/test_parser.py +++ b/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', ) diff --git a/test/test_songbook/content.sb b/test/test_songbook/content.sb index 99042d25..7e5ee2c8 100644 --- a/test/test_songbook/content.sb +++ b/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"], @@ -9,5 +15,4 @@ ["tex", "foo.tex"] ], ["include", "include.sbc"] - ] -} + ] \ No newline at end of file diff --git a/test/test_songbook/content.tex.control b/test/test_songbook/content.tex.control index 02606184..4120642f 100644 --- a/test/test_songbook/content.tex.control +++ b/test/test_songbook/content.tex.control @@ -23,9 +23,9 @@ ]{article} \usepackage[ - chorded, -diagram, +chorded, 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} diff --git a/test/test_songbook/datadir.sb b/test/test_songbook/datadir.sb index f44c3dba..0763bc7a 100644 --- a/test/test_songbook/datadir.sb +++ b/test/test_songbook/datadir.sb @@ -1,6 +1,6 @@ -bookoptions: -- pictures -datadir: -- datadir_datadir -- datadir_datadir2 -lang: en +book: + pictures: yes + datadir: + - datadir_datadir + - datadir_datadir2 + lang: en diff --git a/test/test_songbook/datadir.tex.control b/test/test_songbook/datadir.tex.control index afc99160..56bee48e 100644 --- a/test/test_songbook/datadir.tex.control +++ b/test/test_songbook/datadir.tex.control @@ -24,8 +24,10 @@ ]{article} \usepackage[ - chorded, +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} diff --git a/test/test_songbook/languages.sb b/test/test_songbook/languages.sb index ff589a74..43e7ab85 100644 --- a/test/test_songbook/languages.sb +++ b/test/test_songbook/languages.sb @@ -1,2 +1,3 @@ -datadir: -- languages_datadir +book: + datadir: + - languages_datadir diff --git a/test/test_songbook/languages.tex.control b/test/test_songbook/languages.tex.control index 6f08614f..c8b8cfd2 100644 --- a/test/test_songbook/languages.tex.control +++ b/test/test_songbook/languages.tex.control @@ -23,9 +23,10 @@ ]{article} \usepackage[ - chorded, -diagram, +chorded, 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} diff --git a/test/test_songbook/syntax.sb b/test/test_songbook/syntax.sb index 4b1f4ef2..aca40b87 100644 --- a/test/test_songbook/syntax.sb +++ b/test/test_songbook/syntax.sb @@ -1,3 +1,4 @@ -datadir: -- syntax_datadir -lang: en +book: + datadir: + - syntax_datadir + lang: en diff --git a/test/test_songbook/syntax.tex.control b/test/test_songbook/syntax.tex.control index b2147b06..e142867e 100644 --- a/test/test_songbook/syntax.tex.control +++ b/test/test_songbook/syntax.tex.control @@ -23,9 +23,10 @@ ]{article} \usepackage[ - chorded, -diagram, +chorded, 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} diff --git a/test/test_songbook/unicode.sb b/test/test_songbook/unicode.sb index 19392a88..9f5163c5 100644 --- a/test/test_songbook/unicode.sb +++ b/test/test_songbook/unicode.sb @@ -1,3 +1,4 @@ -datadir: -- unicode_datadir -lang: en +book: + datadir: + - unicode_datadir + lang: en diff --git a/test/test_songbook/unicode.tex.control b/test/test_songbook/unicode.tex.control index 759a15d8..358938b6 100644 --- a/test/test_songbook/unicode.tex.control +++ b/test/test_songbook/unicode.tex.control @@ -23,9 +23,10 @@ ]{article} \usepackage[ - chorded, -diagram, +chorded, 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}