diff --git a/patacrep/authors.py b/patacrep/authors.py index b902bb02..84613a1a 100644 --- a/patacrep/authors.py +++ b/patacrep/authors.py @@ -225,9 +225,12 @@ def processauthors(authors_string, after=None, ignore=None, sep=None): ) ] -def process_listauthors(authors_list): +def process_listauthors(authors_list, after=None, ignore=None, sep=None): """Process a list of authors, and return the list of resulting authors.""" - return sum([ - processauthors(string) - for string in authors_list - ]) + authors = [] + for sublist in [ + processauthors(string, after, ignore, sep) + for string in authors_list + ]: + authors.extend(sublist) + return authors diff --git a/patacrep/build.py b/patacrep/build.py index facf51e8..a16daca2 100644 --- a/patacrep/build.py +++ b/patacrep/build.py @@ -10,7 +10,7 @@ from subprocess import Popen, PIPE, call from patacrep import __DATADIR__, authors, content, errors, files from patacrep.index import process_sxd -from patacrep.templates import TexRenderer +from patacrep.templates import TexBookRenderer from patacrep.songs import DataSubpath LOGGER = logging.getLogger(__name__) @@ -87,7 +87,7 @@ class Songbook(object): # Updating configuration config = DEFAULT_CONFIG.copy() config.update(self.config) - renderer = TexRenderer( + renderer = TexBookRenderer( config['template'], config['datadir'], config['lang'], diff --git a/patacrep/data/examples/example-all.sb b/patacrep/data/examples/example-all.sb index 0b1252aa..ca35b622 100644 --- a/patacrep/data/examples/example-all.sb +++ b/patacrep/data/examples/example-all.sb @@ -7,6 +7,7 @@ ], "booktype" : "chorded", "lang" : "french", +"encoding": "utf8", "authwords" : { "sep" : ["and", "et"] }, diff --git a/patacrep/data/examples/songs/greensleeves.sgc b/patacrep/data/examples/songs/greensleeves.sgc index 7d6b1000..a039cc74 100644 --- a/patacrep/data/examples/songs/greensleeves.sgc +++ b/patacrep/data/examples/songs/greensleeves.sgc @@ -1,7 +1,9 @@ {language : english} {columns : 2} { title : Greensleeves} +{ title : Un sous titre} {artist: Traditionnel} +{artist: Prénom Nom} {cover : traditionnel } {album :Angleterre} @@ -10,7 +12,7 @@ A[Am]las, my love, ye [G]do me wrong To [Am]cast me oft dis[E]curteously -And [Am]{I have} loved [G]you so long +And [Am]I have loved [G]you so long De[Am]lighting [E]in your [Am]companie {start_of_chorus} @@ -35,8 +37,15 @@ The [Am]cloth so fine as [E]fine might be I [Am]gave thee jewels [G]for thy chest And [Am]all this [E]cost I [Am]spent on thee +{c:test of comment} + +{gc: test of guitar comment} + +{image: traditionnel} Thy [Am]smock of silke, both [G]faire and white With [Am]gold embrodered [E]gorgeously Thy [Am]peticote of [G]sendall right -And [Am]this I [E]bought thee [Am]gladly \ No newline at end of file +And [Am]this I [E]bought thee [Am]gladly + + diff --git a/patacrep/songs/__init__.py b/patacrep/songs/__init__.py index e8931d34..c85e935a 100644 --- a/patacrep/songs/__init__.py +++ b/patacrep/songs/__init__.py @@ -8,7 +8,7 @@ import os import pickle import re -from patacrep.authors import processauthors +from patacrep.authors import process_listauthors from patacrep import files, encoding LOGGER = logging.getLogger(__name__) @@ -130,13 +130,12 @@ class Song(Content): )) # Data extraction from the latex song - self.data = self.DEFAULT_DATA - self.data['@path'] = self.fullpath - self.data.update(self.parse( - encoding.open_read(self.fullpath).read() - )) - self.titles = self.data['@titles'] - self.languages = self.data['@languages'] + self.titles = [] + self.data = {} + self.cached = None + self.parse(config) + + # Post processing of data self.datadir = datadir self.unprefixed_titles = [ unprefixed_title( @@ -146,17 +145,12 @@ class Song(Content): for title in self.titles ] - self.subpath = subpath - self.authors = processauthors( + self.authors = process_listauthors( self.authors, **config["_compiled_authwords"] ) # Cache management - - #: Special attribute to allow plugins to store cached data - self.cached = None - self._version = self.CACHE_VERSION self._write_cache() @@ -181,11 +175,24 @@ class Song(Content): Arguments: - output: Name of the output file. """ - return NotImplementedError() - - def parse(self, content): # pylint: disable=no-self-use, unused-argument - """Parse song, and return a dictionary of its data.""" - return NotImplementedError() + raise NotImplementedError() + + def parse(self, config): # pylint: disable=no-self-use + """Parse song. + + It set the following attributes: + + - titles: the list of (raw) titles. This list will be processed to + remove prefixes. + - languages: the list of languages used in the song, as languages + recognized by the LaTeX babel package. + - authors: the list of (raw) authors. This list will be processed to + 'clean' it (see function :func:`patacrep.authors.processauthors`). + - data: song metadata. Used (among others) to sort the songs. + - cached: additional data that will be cached. Thus, data stored in + this attribute must be picklable. + """ + raise NotImplementedError() def unprefixed_title(title, prefixes): """Remove the first prefix of the list in the beginning of title (if any). diff --git a/patacrep/songs/chordpro/__init__.py b/patacrep/songs/chordpro/__init__.py index e76960ac..cd3f0d3b 100644 --- a/patacrep/songs/chordpro/__init__.py +++ b/patacrep/songs/chordpro/__init__.py @@ -1,21 +1,50 @@ +"""Chordpro parser""" -from patacrep import encoding +from jinja2 import Environment, FileSystemLoader +import pkg_resources +import os + +from patacrep import encoding, files from patacrep.songs import Song from patacrep.songs.chordpro.syntax import parse_song +from patacrep.templates import TexRenderer class ChordproSong(Song): """Chordpros song parser.""" - def parse(self): + def parse(self, config): """Parse content, and return the dictinory of song data.""" with encoding.open_read(self.fullpath, encoding=self.encoding) as song: - self.data = parse_song(song.read(), self.fullpath) - print(type(self.data), self.data) - import sys; sys.exit(1) - self.languages = self.data['@languages'] - del self.data['@languages'] - self.authors = self.data['by'] - del self.data['by'] + song = parse_song(song.read(), self.fullpath) + self.authors = song.authors + self.titles = song.titles + self.languages = song.get_directives('language') + self.data = dict([meta.as_tuple for meta in song.meta]) + self.cached = { + 'song': song, + } + #: Main language + self.language = song.get_directive('language', config['lang']) + + def tex(self, output): + context = { + 'language': self.language, + 'columns': self.cached['song'].get_directive('columns', 1), + "path": files.relpath(self.fullpath, os.path.dirname(output)), + "titles": r"\\".join(self.titles), + "authors": ", ".join(["{} {}".format(name[1], name[0]) for name in self.authors]), + "metadata": self.data, + "beginsong": self.cached['song'].meta_beginsong(), + "content": self.cached['song'].content, + } + return TexRenderer( + template="chordpro.tex", + encoding='utf8', + texenv=Environment(loader=FileSystemLoader(os.path.join( + os.path.abspath(pkg_resources.resource_filename(__name__, 'data')), + 'latex' + ))), + ).template.render(context) SONG_PARSERS = { 'sgc': ChordproSong, diff --git a/patacrep/songs/chordpro/ast.py b/patacrep/songs/chordpro/ast.py index 05affee6..47ffb8df 100644 --- a/patacrep/songs/chordpro/ast.py +++ b/patacrep/songs/chordpro/ast.py @@ -1,18 +1,37 @@ -# -*- coding: utf-8 -*- """Abstract Syntax Tree for ChordPro code.""" +# pylint: disable=too-few-public-methods + import functools +import logging +import os + +LOGGER = logging.getLogger() def _indent(string): + """Return and indented version of argument.""" return "\n".join([" {}".format(line) for line in string.split('\n')]) +#: List of properties that are to be displayed in the flow of the song (not as +#: metadata at the beginning or end of song. INLINE_PROPERTIES = { - "lilypond", + "partition", "comment", "guitar_comment", "image", } +#: List of properties that are listed in the `\beginsong` LaTeX directive. +BEGINSONG_PROPERTIES = { + "album", + "copyright", + "cov", + "vcov", + "tag", + } + +#: Some directive have alternative names. For instance `{title: Foo}` and `{t: +#: Foo}` are equivalent. DIRECTIVE_SHORTCUTS = { "t": "title", "st": "subtitle", @@ -20,25 +39,40 @@ DIRECTIVE_SHORTCUTS = { "by": "artist", "c": "comment", "gc": "guitar_comment", + "cover": "cov", + "vcover": "vcov", } def directive_name(text): - if text in DIRECTIVE_SHORTCUTS: - return DIRECTIVE_SHORTCUTS[text] - return text + """Return name of the directive, considering eventual shortcuts.""" + return DIRECTIVE_SHORTCUTS.get(text, text) class AST: + """Generic object representing elements of the song.""" + _template = None inline = False + def template(self, extension): + """Return the template to be used to render this object.""" + if self._template is None: + LOGGER.warning("No template defined for {}.".format(self.__class__)) + base = "error" + else: + base = self._template + return "content_{}.{}".format(base, extension) + class Line(AST): """A line is a sequence of (possibly truncated) words, spaces and chords.""" + _template = "line" + def __init__(self): super().__init__() self.line = [] def prepend(self, data): + """Add an object at the beginning of line.""" self.line.insert(0, data) return self @@ -46,6 +80,7 @@ class Line(AST): return "".join([str(item) for item in self.line]) def strip(self): + """Remove spaces at the beginning and end of line.""" while True: if not self.line: return self @@ -63,6 +98,7 @@ class LineElement(AST): class Word(LineElement): """A chunk of word.""" + _template = "word" def __init__(self, value): super().__init__() @@ -73,6 +109,7 @@ class Word(LineElement): class Space(LineElement): """A space between words""" + _template = "space" def __init__(self): super().__init__() @@ -83,6 +120,8 @@ class Space(LineElement): class Chord(LineElement): """A chord.""" + _template = "chord" + def __init__(self, value): super().__init__() self.value = value @@ -92,59 +131,95 @@ class Chord(LineElement): class Verse(AST): """A verse (or bridge, or chorus)""" + _template = "verse" type = "verse" inline = True - def __init__(self, block=None): + def __init__(self): super().__init__() - self.lines = [] # TODO check block + self.lines = [] def prepend(self, data): + """Add data at the beginning of verse.""" self.lines.insert(0, data) return self def __str__(self): return '{{start_of_{type}}}\n{content}\n{{end_of_{type}}}'.format( - type = self.type, - content = _indent("\n".join([str(line) for line in self.lines])), + type=self.type, + content=_indent("\n".join([str(line) for line in self.lines])), ) class Chorus(Verse): + """Chorus""" type = 'chorus' class Bridge(Verse): + """Bridge""" type = 'bridge' class Song(AST): - """A song""" - + r"""A song + + Attributes: + - content: the song content, as a list of objects `foo` such that + `foo.inline` is True. + - titles: The list of titles + - language: The language (if set), None otherwise + - authors: The list of authors + - meta_beginsong: The list of directives that are to be set in the + `\beginsong{}` LaTeX directive. + - meta: Every other metadata. + """ + + #: Some directives are added to the song using special methods. METADATA_TYPE = { "title": "add_title", "subtitle": "add_subtitle", - "language": "add_language", "artist": "add_author", + "key": "add_key", } - def __init__(self): + #: Some directives have to be processed before being considered. + PROCESS_DIRECTIVE = { + "cov": "_process_relative", + "partition": "_process_relative", + "image": "_process_relative", + } + + def __init__(self, filename): super().__init__() self.content = [] self.meta = [] self._authors = [] self._titles = [] self._subtitles = [] - self._languages = set() + self._keys = [] + self.filename = filename def add(self, data): + """Add an element to the song""" + if isinstance(data, Directive): + # Some directives are preprocessed + name = directive_name(data.keyword) + if name in self.PROCESS_DIRECTIVE: + data = getattr(self, self.PROCESS_DIRECTIVE[name])(data) + if data is None: + # New line if not (self.content and isinstance(self.content[0], Newline)): self.content.insert(0, Newline()) elif isinstance(data, Line): + # Add a new line, maybe in the current verse. if not (self.content and isinstance(self.content[0], Verse)): self.content.insert(0, Verse()) self.content[0].prepend(data.strip()) elif data.inline: + # Add an object in the content of the song. self.content.insert(0, data) elif isinstance(data, Directive): + # Add a metadata directive. Some of them are added using special + # methods listed in ``METADATA_TYPE``. name = directive_name(data.keyword) if name in self.METADATA_TYPE: getattr(self, self.METADATA_TYPE[name])(*data.as_tuple) @@ -155,12 +230,13 @@ class Song(AST): return self def str_meta(self): + """Return an iterator over *all* metadata, as strings.""" for title in self.titles: yield "{{title: {}}}".format(title) - for language in sorted(self.languages): - yield "{{language: {}}}".format(language) for author in self.authors: yield "{{by: {}}}".format(author) + for key in sorted(self.keys): + yield "{{key: {}}}".format(str(key)) for key in sorted(self.meta): yield str(key) @@ -175,32 +251,84 @@ class Song(AST): def add_title(self, __ignored, title): + """Add a title""" self._titles.insert(0, title) def add_subtitle(self, __ignored, title): + """Add a subtitle""" self._subtitles.insert(0, title) @property def titles(self): + """Return the list of titles (and subtitles).""" return self._titles + self._subtitles def add_author(self, __ignored, title): + """Add an auhor.""" self._authors.insert(0, title) @property def authors(self): + """Return the list of (raw) authors.""" return self._authors - def add_language(self, __ignored, language): - self._languages.add(language) + def get_directive(self, key, default=None): + """Return the first directive with a given key.""" + for directive in self.meta: + if directive.keyword == directive_name(key): + return directive.argument + return default + + def get_directives(self, key): + """Return the list of directives with a given key.""" + values = [] + for directive in self.meta: + if directive.keyword == directive_name(key): + values.append(directive.argument) + return values + + def add_key(self, __ignored, argument): + """Add a new {key: foo: bar} directive.""" + key, *argument = argument.split(":") + self._keys.append(Directive( + key.strip(), + ":".join(argument).strip(), + )) @property - def languages(self): - return self._languages - - + def keys(self): + """Return the list of keys. + + That is, directive that where given of the form ``{key: foo: bar}``. + """ + return self._keys + + def meta_beginsong(self): + r"""Return the meta information to be put in \beginsong.""" + for directive in BEGINSONG_PROPERTIES: + if self.get_directive(directive) is not None: + yield (directive, self.get_directive(directive)) + for (key, value) in self.keys: + yield (key, value) + + + def _process_relative(self, directive): + """Return the directive, in which the argument is given relative to file + + This argument is expected to be a path (as a string). + """ + return Directive( + directive.keyword, + os.path.join( + os.path.dirname(self.filename), + directive.argument, + ), + ) class Newline(AST): + """New line""" + _template = "newline" + def __str__(self): return "" @@ -208,22 +336,38 @@ class Newline(AST): class Directive(AST): """A directive""" - def __init__(self): + def __init__(self, keyword="", argument=None): super().__init__() - self.keyword = "" - self.argument = None + self._keyword = None + self.keyword = keyword + self.argument = argument + + @property + def _template(self): + """Name of the template to use to render this keyword. + + This only applies if ``self.inline == True`` + """ + return self.keyword @property def keyword(self): + """Keyword of the directive.""" return self._keyword @property def inline(self): + """True iff this directive is to be rendered in the flow on the song. + """ return self.keyword in INLINE_PROPERTIES @keyword.setter def keyword(self, value): - self._keyword = value.strip() + """self.keyword setter + + Replace keyword by its canonical name if it is a shortcut. + """ + self._keyword = directive_name(value.strip()) def __str__(self): if self.argument is not None: @@ -236,6 +380,7 @@ class Directive(AST): @property def as_tuple(self): + """Return the directive as a tuple.""" return (self.keyword, self.argument) def __eq__(self, other): @@ -254,6 +399,7 @@ class Tab(AST): self.content = [] def prepend(self, data): + """Add an element at the beginning of content.""" self.content.insert(0, data) return self diff --git a/patacrep/songs/chordpro/data/latex/chordpro.tex b/patacrep/songs/chordpro/data/latex/chordpro.tex new file mode 100644 index 00000000..77784a67 --- /dev/null +++ b/patacrep/songs/chordpro/data/latex/chordpro.tex @@ -0,0 +1,27 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% ((path)) + +(* if language is defined *) +\selectlanguage{((language))} +(* endif *) +\songcolumns{((metadata.columns))} + +\beginsong{((titles))}[ + by={((authors))}, + (* for (key, argument) in beginsong *) + ((key))={((argument))}, + (* endfor *) + ] + + (* if (metadata.cov is defined) or (metadata.vcov is defined) *) + \cover + (* endif *) + + (* for content in content *) + (* include content.template("tex") *) + (* endfor *) + +\endsong +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + diff --git a/patacrep/songs/chordpro/data/latex/content_chord.tex b/patacrep/songs/chordpro/data/latex/content_chord.tex new file mode 100644 index 00000000..bd2bd8b8 --- /dev/null +++ b/patacrep/songs/chordpro/data/latex/content_chord.tex @@ -0,0 +1 @@ +\[(( content.value ))] diff --git a/patacrep/songs/chordpro/data/latex/content_comment.tex b/patacrep/songs/chordpro/data/latex/content_comment.tex new file mode 100644 index 00000000..46fb1f24 --- /dev/null +++ b/patacrep/songs/chordpro/data/latex/content_comment.tex @@ -0,0 +1 @@ +\textnote{(( content.argument ))} diff --git a/patacrep/songs/chordpro/data/latex/content_error.tex b/patacrep/songs/chordpro/data/latex/content_error.tex new file mode 100644 index 00000000..a41faeb5 --- /dev/null +++ b/patacrep/songs/chordpro/data/latex/content_error.tex @@ -0,0 +1,3 @@ + +ERROR : Template not found for \verb+(( content.__class__))+. See the logs for details. + diff --git a/patacrep/songs/chordpro/data/latex/content_guitar_comment.tex b/patacrep/songs/chordpro/data/latex/content_guitar_comment.tex new file mode 100644 index 00000000..29c40ebd --- /dev/null +++ b/patacrep/songs/chordpro/data/latex/content_guitar_comment.tex @@ -0,0 +1 @@ +\musicnote{(( content.argument ))} diff --git a/patacrep/songs/chordpro/data/latex/content_image.tex b/patacrep/songs/chordpro/data/latex/content_image.tex new file mode 100644 index 00000000..336233d5 --- /dev/null +++ b/patacrep/songs/chordpro/data/latex/content_image.tex @@ -0,0 +1 @@ +\image{(( content.argument ))} diff --git a/patacrep/songs/chordpro/data/latex/content_line.tex b/patacrep/songs/chordpro/data/latex/content_line.tex new file mode 100644 index 00000000..9b0c6af1 --- /dev/null +++ b/patacrep/songs/chordpro/data/latex/content_line.tex @@ -0,0 +1 @@ +(* for content in content.line *)(* include content.template("tex") *)(* endfor *) diff --git a/patacrep/songs/chordpro/data/latex/content_newline.tex b/patacrep/songs/chordpro/data/latex/content_newline.tex new file mode 100644 index 00000000..139597f9 --- /dev/null +++ b/patacrep/songs/chordpro/data/latex/content_newline.tex @@ -0,0 +1,2 @@ + + diff --git a/patacrep/songs/chordpro/data/latex/content_partition.tex b/patacrep/songs/chordpro/data/latex/content_partition.tex new file mode 100644 index 00000000..98bfed57 --- /dev/null +++ b/patacrep/songs/chordpro/data/latex/content_partition.tex @@ -0,0 +1 @@ +\lilypond{((content.argument))} diff --git a/patacrep/songs/chordpro/data/latex/content_space.tex b/patacrep/songs/chordpro/data/latex/content_space.tex new file mode 100644 index 00000000..8d1c8b69 --- /dev/null +++ b/patacrep/songs/chordpro/data/latex/content_space.tex @@ -0,0 +1 @@ + diff --git a/patacrep/songs/chordpro/data/latex/content_verse.tex b/patacrep/songs/chordpro/data/latex/content_verse.tex new file mode 100644 index 00000000..4b4cecbd --- /dev/null +++ b/patacrep/songs/chordpro/data/latex/content_verse.tex @@ -0,0 +1,7 @@ +\begin{(( content.type ))} + (* for content in content.lines *) + (* include content.template("tex") *) + + (* endfor *) +\end{(( content.type ))} + diff --git a/patacrep/songs/chordpro/data/latex/content_word.tex b/patacrep/songs/chordpro/data/latex/content_word.tex new file mode 100644 index 00000000..d9dd7a30 --- /dev/null +++ b/patacrep/songs/chordpro/data/latex/content_word.tex @@ -0,0 +1 @@ +(( content.value )) diff --git a/patacrep/songs/chordpro/lexer.py b/patacrep/songs/chordpro/lexer.py index 726c24e4..58792fa2 100644 --- a/patacrep/songs/chordpro/lexer.py +++ b/patacrep/songs/chordpro/lexer.py @@ -26,6 +26,7 @@ tokens = ( class ChordProLexer: """ChordPro Lexer class""" + # pylint: disable=too-many-public-methods tokens = tokens @@ -44,18 +45,23 @@ class ChordProLexer: t_directive_KEYWORD = r'[a-zA-Z_]+' t_directiveargument_TEXT = r'[^}]+' - def t_SOC(self, token): + @staticmethod + def t_SOC(token): r'{(soc|start_of_chorus)}' return token - def t_EOC(self, token): + + @staticmethod + def t_EOC(token): r'{(eoc|end_of_chorus)}' return token - def t_SOB(self, token): + @staticmethod + def t_SOB(token): r'{(sob|start_of_bridge)}' return token - def t_EOB(self, token): + @staticmethod + def t_EOB(token): r'{(eob|end_of_bridge)}' return token @@ -69,7 +75,8 @@ class ChordProLexer: self.lexer.pop_state() return token - def t_tablature_SPACE(self, token): + @staticmethod + def t_tablature_SPACE(token): r'[ \t]+' return token @@ -96,11 +103,11 @@ class ChordProLexer: r'[^{}\n\][\t ]+' return token - def t_LBRACKET(self, token): + def t_LBRACKET(self, __token): r'\[' self.lexer.push_state('chord') - def t_chord_RBRACKET(self, token): + def t_chord_RBRACKET(self, __token): r'\]' self.lexer.pop_state() @@ -149,6 +156,6 @@ class ChordProLexer: LOGGER.error("Illegal character '{}' in directive..".format(token.value[0])) token.lexer.skip(1) - @staticmethod - def t_directiveargument_error(token): - return t_directive_error(token) + def t_directiveargument_error(self, token): + """Manage errors""" + return self.t_directive_error(token) diff --git a/patacrep/songs/chordpro/syntax.py b/patacrep/songs/chordpro/syntax.py index 32fa2c42..8a964564 100644 --- a/patacrep/songs/chordpro/syntax.py +++ b/patacrep/songs/chordpro/syntax.py @@ -6,7 +6,6 @@ import ply.yacc as yacc from patacrep.errors import SongbookError from patacrep.songs.chordpro import ast -from patacrep.songs.chordpro import ast from patacrep.songs.chordpro.lexer import tokens, ChordProLexer LOGGER = logging.getLogger() @@ -50,14 +49,13 @@ class Parser: ) ) - @staticmethod - def p_song(symbols): + def p_song(self, symbols): """song : block song | empty """ #if isinstance(symbols[1], str): if len(symbols) == 2: - symbols[0] = ast.Song() + symbols[0] = ast.Song(self.filename) else: symbols[0] = symbols[2].add(symbols[1]) @@ -249,19 +247,13 @@ class Parser: """empty :""" symbols[0] = None -def lex_song(content): - # TODO delete - lex = ChordProLexer().lexer - lex.input(content) - while 1: - tok = lex.token() - if not tok: break - print(tok) - def parse_song(content, filename=None): """Parse song and return its metadata.""" - return yacc.yacc(module=Parser(filename)).parse( - content, + return yacc.yacc( + module=Parser(filename), debug=0, + write_tables=0, + ).parse( + content, lexer=ChordProLexer().lexer, ) diff --git a/patacrep/songs/chordpro/test/__init__.py b/patacrep/songs/chordpro/test/__init__.py index e69de29b..b73d2af8 100644 --- a/patacrep/songs/chordpro/test/__init__.py +++ b/patacrep/songs/chordpro/test/__init__.py @@ -0,0 +1 @@ +"""Test for chordpro parser""" diff --git a/patacrep/songs/chordpro/test/greensleeves.txt b/patacrep/songs/chordpro/test/greensleeves.txt index 38564149..069fbba8 100644 --- a/patacrep/songs/chordpro/test/greensleeves.txt +++ b/patacrep/songs/chordpro/test/greensleeves.txt @@ -1,13 +1,14 @@ {title: Greensleeves} {title: Un autre sous-titre} {title: Un sous titre} -{language: english} {by: Traditionnel} {album: Angleterre} {columns: 2} -{cover: traditionnel} -{partition: greensleeves.ly} +{cov: DIRNAME/traditionnel} +{language: english} ======== +{partition: DIRNAME/greensleeves.ly} + {start_of_verse} A[Am]las, my love, ye [G]do me wrong To [Am]cast me oft dis[E]curteously diff --git a/patacrep/songs/chordpro/test/metadata.sgc b/patacrep/songs/chordpro/test/metadata.sgc index c892faf0..eee1bf9d 100644 --- a/patacrep/songs/chordpro/test/metadata.sgc +++ b/patacrep/songs/chordpro/test/metadata.sgc @@ -13,8 +13,8 @@ {cover: Cover} {vcover: VCover} {capo: Capo} -{foo: Foo} +{key: foo: Foo} {comment: Comment} {guitar_comment: GuitarComment} {image: Image} -{lilypond: Lilypond} +{partition: Lilypond} diff --git a/patacrep/songs/chordpro/test/metadata.txt b/patacrep/songs/chordpro/test/metadata.txt index 8d73f4b8..1ba5f5c6 100644 --- a/patacrep/songs/chordpro/test/metadata.txt +++ b/patacrep/songs/chordpro/test/metadata.txt @@ -4,18 +4,18 @@ {title: Subtitle3} {title: Subtitle4} {title: Subtitle5} -{language: english} -{language: french} {by: Author1} {by: Author2} +{key: {foo: Foo}} {album: Albom} {capo: Capo} {copyright: Copyright} -{cover: Cover} -{foo: Foo} -{vcover: VCover} +{cov: DIRNAME/Cover} +{language: english} +{language: french} +{vcov: VCover} ======== {comment: Comment} {guitar_comment: GuitarComment} -{image: Image} -{lilypond: Lilypond} +{image: DIRNAME/Image} +{partition: DIRNAME/Lilypond} diff --git a/patacrep/songs/chordpro/test/test_parser.py b/patacrep/songs/chordpro/test/test_parser.py index 85ad4108..2743522e 100644 --- a/patacrep/songs/chordpro/test/test_parser.py +++ b/patacrep/songs/chordpro/test/test_parser.py @@ -1,36 +1,56 @@ +"""Tests for the chordpro parser.""" + +# pylint: disable=too-few-public-methods + import glob import os import unittest from patacrep.songs.chordpro import syntax as chordpro -class ParserTestCase(unittest.TestCase): +class ParserTxtRenderer(unittest.TestCase): + """Test parser, and renderer as a txt file.""" + + maxDiff = None + + def __init__(self, methodname="runTest", basename=None): + super().__init__(methodname) + self.basename = basename + + def shortDescription(self): + return "Parsing file '{}.txt'.".format(self.basename) + + def runTest(self): + """Test txt output (default, debug output).""" + # pylint: disable=invalid-name + + if self.basename is None: + return + with open("{}.sgc".format(self.basename), 'r', encoding='utf8') as sourcefile: + with open("{}.txt".format(self.basename), 'r', encoding='utf8') as expectfile: + #print(os.path.basename(sourcefile.name)) + #with open("{}.txt.diff".format(self.basename), 'w', encoding='utf8') as difffile: + # difffile.write( + # str(chordpro.parse_song( + # sourcefile.read(), + # os.path.basename(sourcefile.name), + # )).strip() + # ) + # sourcefile.seek(0) + self.assertMultiLineEqual( + str(chordpro.parse_song( + sourcefile.read(), + os.path.abspath(sourcefile.name), + )).strip(), + expectfile.read().strip().replace("DIRNAME", os.path.dirname(self.basename)), + ) - def test_txt(self): - for txt in sorted(glob.glob(os.path.join( - os.path.dirname(__file__), - '*.txt', +def load_tests(__loader, tests, __pattern): + """Load several tests given test files present in the directory.""" + # Load all txt files as tests + for txt in sorted(glob.glob(os.path.join( + os.path.dirname(__file__), + '*.txt', ))): - basename = txt[:-len('.txt')] - with open("{}.sgc".format(basename), 'r', encoding='utf8') as sourcefile: - with open("{}.txt".format(basename), 'r', encoding='utf8') as expectfile: - #print(os.path.basename(sourcefile.name)) - #with open("{}.txt.diff".format(basename), 'w', encoding='utf8') as difffile: - # difffile.write( - # str(chordpro.parse_song( - # sourcefile.read(), - # os.path.basename(sourcefile.name), - # )).strip() - # ) - # sourcefile.seek(0) - self.assertMultiLineEqual( - str(chordpro.parse_song( - sourcefile.read(), - os.path.basename(sourcefile.name), - )).strip(), - expectfile.read().strip(), - ) - - def test_tex(self): - # TODO - pass + tests.addTest(ParserTxtRenderer(basename=txt[:-len('.txt')])) + return tests diff --git a/patacrep/songs/latex/__init__.py b/patacrep/songs/latex/__init__.py index 93fbccc7..bbab88cc 100644 --- a/patacrep/songs/latex/__init__.py +++ b/patacrep/songs/latex/__init__.py @@ -14,7 +14,7 @@ from patacrep.songs import Song class LatexSong(Song): """LaTeX song parser.""" - def parse(self, content): + def parse(self, __config): """Parse content, and return the dictinory of song data.""" with encoding.open_read(self.fullpath, encoding=self.encoding) as song: self.data = parse_song(song.read(), self.fullpath) diff --git a/patacrep/templates.py b/patacrep/templates.py index ed48529f..6849ad36 100644 --- a/patacrep/templates.py +++ b/patacrep/templates.py @@ -64,9 +64,29 @@ def _escape_tex(value): return newval -class TexRenderer(object): +class TexRenderer: """Render a template to a LaTeX file.""" + def __init__(self, template, texenv, encoding=None): + self.encoding = encoding + self.texenv = texenv + self.texenv.block_start_string = '(*' + self.texenv.block_end_string = '*)' + self.texenv.variable_start_string = '((' + self.texenv.variable_end_string = '))' + self.texenv.comment_start_string = '(% comment %)' + self.texenv.comment_end_string = '(% endcomment %)' + self.texenv.line_comment_prefix = '%!' + self.texenv.filters['escape_tex'] = _escape_tex + self.texenv.trim_blocks = True + self.texenv.lstrip_blocks = True + self.texenv.globals["path2posix"] = files.path2posix + self.template = self.texenv.get_template(template) + + +class TexBookRenderer(TexRenderer): + """Tex renderer for the whole songbook""" + def __init__(self, template, datadirs, lang, encoding=None): '''Start a new jinja2 environment for .tex creation. @@ -78,29 +98,15 @@ class TexRenderer(object): - encoding: if set, encoding of the template. ''' self.lang = lang - self.encoding = encoding # Load templates in filesystem ... loaders = [FileSystemLoader(os.path.join(datadir, 'templates')) for datadir in datadirs] - self.texenv = Environment( + texenv = Environment( loader=ChoiceLoader(loaders), extensions=[VariablesExtension], ) - self.texenv.block_start_string = '(*' - self.texenv.block_end_string = '*)' - self.texenv.variable_start_string = '((' - self.texenv.variable_end_string = '))' - self.texenv.comment_start_string = '(% comment %)' - self.texenv.comment_end_string = '(% endcomment %)' - self.texenv.line_comment_prefix = '%!' - self.texenv.filters['escape_tex'] = _escape_tex - self.texenv.trim_blocks = True - self.texenv.lstrip_blocks = True - - self.texenv.globals["path2posix"] = files.path2posix - try: - self.template = self.texenv.get_template(template) + super().__init__(template, texenv, encoding) except TemplateNotFound as exception: # Only works if all loaders are FileSystemLoader(). paths = [ diff --git a/patacrep/test.py b/patacrep/test.py new file mode 100644 index 00000000..ce135fef --- /dev/null +++ b/patacrep/test.py @@ -0,0 +1,26 @@ +"""Tests""" + +import doctest +import os +import unittest + +import patacrep + +def suite(): + """Return a TestSuite object, to test whole `patacrep` package. + + Both unittest and doctest are tested. + """ + test_loader = unittest.defaultTestLoader + return test_loader.discover(os.path.dirname(__file__)) + +def load_tests(__loader, tests, __pattern): + """Load tests (unittests and doctests).""" + # Loading doctests + tests.addTests(doctest.DocTestSuite(patacrep)) + + # Unittests are loaded by default + return tests + +if __name__ == "__main__": + unittest.TextTestRunner().run(suite()) diff --git a/setup.py b/setup.py index d153d49e..5a315fbd 100755 --- a/setup.py +++ b/setup.py @@ -38,6 +38,7 @@ setup( "Programming Language :: Python :: 3.4", "Topic :: Utilities", ], - platforms=["GNU/Linux", "Windows", "MacOsX"] + platforms=["GNU/Linux", "Windows", "MacOsX"], + test_suite="patacrep.test.suite", long_description = open("README.rst", "r").read(), )