From 51d4562901e6f6d4ca02660fc14211bdec778275 Mon Sep 17 00:00:00 2001 From: Louis Date: Mon, 19 Oct 2015 23:50:24 +0200 Subject: [PATCH] Create one Song class per couple parser/renderer Instead of one song class per parser, which renders several formats --- patacrep/build.py | 4 +- patacrep/content/song.py | 2 +- patacrep/files.py | 14 +-- patacrep/songs/__init__.py | 62 ++++++++----- patacrep/songs/chordpro/__init__.py | 90 +++++++++++++------ .../songs/chordpro/data/html/content_image | 5 ++ .../chordpro/data/html/content_metadata_cover | 9 +- .../chordpro/data/html/content_partition | 7 +- .../songs/chordpro/data/latex/content_image | 7 +- .../chordpro/data/latex/content_partition | 5 ++ patacrep/songs/chordpro/data/latex/song | 7 +- patacrep/songs/convert/__main__.py | 19 ++-- patacrep/songs/latex/__init__.py | 18 ++-- .../test_chordpro/datadir/img/greensleeves.ly | 0 .../datadir/img/traditionnel.png | 0 test/test_chordpro/greensleeves.tex | 2 +- test/test_chordpro/metadata.sgc | 6 +- test/test_chordpro/metadata.source | 6 +- test/test_chordpro/metadata.tex | 6 +- test/test_chordpro/metadata_cover.png | 0 test/test_chordpro/metadata_image.png | 0 test/test_chordpro/metadata_lilypond.ly | 0 test/test_chordpro/test_parser.py | 19 +++- 23 files changed, 194 insertions(+), 94 deletions(-) create mode 100644 test/test_chordpro/datadir/img/greensleeves.ly create mode 100644 test/test_chordpro/datadir/img/traditionnel.png create mode 100644 test/test_chordpro/metadata_cover.png create mode 100644 test/test_chordpro/metadata_image.png create mode 100644 test/test_chordpro/metadata_lilypond.ly diff --git a/patacrep/build.py b/patacrep/build.py index 492f30fc..8151f797 100644 --- a/patacrep/build.py +++ b/patacrep/build.py @@ -110,8 +110,8 @@ class Songbook(object): config['_song_plugins'] = files.load_plugins( datadirs=config.get('datadir', []), root_modules=['songs'], - keyword='SONG_PARSERS', - ) + keyword='SONG_RENDERERS', + )['latex'] # Configuration set config['render'] = content.render diff --git a/patacrep/content/song.py b/patacrep/content/song.py index 9101425d..d6bff200 100755 --- a/patacrep/content/song.py +++ b/patacrep/content/song.py @@ -43,7 +43,7 @@ class SongRenderer(Content): """).format( separator="%"*80, path=self.song.subpath, - song=self.song.render(output=context['filename'], output_format="latex"), + song=self.song.render(output=context['filename']), ) #pylint: disable=unused-argument diff --git a/patacrep/files.py b/patacrep/files.py index e77c3ab2..1116c8b0 100644 --- a/patacrep/files.py +++ b/patacrep/files.py @@ -8,6 +8,8 @@ import posixpath import re import sys +from patacrep import utils + LOGGER = logging.getLogger(__name__) def recursive_find(root_directory, extensions): @@ -104,7 +106,7 @@ def load_plugins(datadirs, root_modules, keyword): - keys are the keywords ; - values are functions triggered when this keyword is met. """ - plugins = {} + plugins = utils.DictOfDict() datadir_path = [ os.path.join(datadir, "python", *root_modules) @@ -121,13 +123,5 @@ def load_plugins(datadirs, root_modules, keyword): prefix="patacrep.{}.".format(".".join(root_modules)) ): if hasattr(module, keyword): - for (key, value) in getattr(module, keyword).items(): - if key in plugins: - LOGGER.warning( - "File %s: Keyword '%s' is already used. Ignored.", - module.__file__, - key, - ) - continue - plugins[key] = value + plugins.update(getattr(module, keyword)) return plugins diff --git a/patacrep/songs/__init__.py b/patacrep/songs/__init__.py index 68d5a960..05862287 100644 --- a/patacrep/songs/__init__.py +++ b/patacrep/songs/__init__.py @@ -167,16 +167,12 @@ class Song: def __repr__(self): return repr((self.titles, self.data, self.fullpath)) - def render(self, output_format, output=None, *args, **kwargs): + def render(self, output=None, *args, **kwargs): """Return the code rendering this song. Arguments: - - output_format: Format of the output file (latex, chordpro...) - output: Name of the output file, or `None` if irrelevant. """ - method = "render_{}".format(output_format) - if hasattr(self, method): - return getattr(self, method)(output, *args, **kwargs) raise NotImplementedError() def _parse(self, config): # pylint: disable=no-self-use @@ -204,15 +200,24 @@ class Song: if os.path.isdir(fullpath): yield fullpath - def search_file(self, filename, extensions=None, directories=None): + def search_datadir_file(self, filename, extensions=None, directories=None): """Search for a file name. :param str filename: The name, as provided in the chordpro file (with or without extension). :param list extensions: Possible extensions (with '.'). Default is no extension. :param iterator directories: Other directories where to search for the file The directory where the Song file is stored is added to the list. - - Returns None if nothing found. + :return: A tuple `(datadir, filename, extension)` if file has been + found. It is guaranteed that `os.path.join(datadir, + filename+extension)` is a (relative or absolute) valid path to an + existing filename. + * `datadir` is the datadir in which the file has been found. Can be + the empty string. + * `filename` is the filename, relative to the datadir. + * `extension` is the extension that is to be appended to the + filename to get the real filename. Can be the empty string. + + Raise `FileNotFoundError` if nothing found. This function can also be used as a preprocessor for a renderer: for instance, it can compile a file, place it in a temporary folder, and @@ -223,28 +228,43 @@ class Song: if directories is None: directories = self.config['datadir'] + for directory in directories: + for extension in extensions: + if os.path.isfile(os.path.join(directory, filename + extension)): + return directory, filename, extension + songdir = os.path.dirname(self.fullpath) + for extension in extensions: + if os.path.isfile(os.path.join(songdir, filename + extension)): + return "", os.path.join(songdir, filename), extension - for directory in [songdir] + list(directories): - for extension in extensions: - fullpath = os.path.join(directory, filename + extension) - if os.path.isfile(fullpath): - return os.path.abspath(fullpath) - return None + raise FileNotFoundError(filename) + + def search_file(self, filename, extensions=None, *, datadirs=None): + """Return the path to a file present in a datadir. - def search_image(self, filename, none_if_not_found=False): + Implementation is specific to each renderer, as: + - some renderers can preprocess files; + - some renderers can return the absolute path, other can return something else; + - etc. + """ + raise NotImplementedError() + + def search_image(self, filename): """Search for an image file""" - filepath = self.search_file( + return self.search_file( filename, ['', '.jpg', '.png'], - self.get_datadirs('img'), + datadirs=self.get_datadirs('img'), ) - return filepath if none_if_not_found or filepath else filename - def search_partition(self, filename, none_if_not_found=False): + def search_partition(self, filename): """Search for a lilypond file""" - filepath = self.search_file(filename, ['', '.ly']) - return filepath if none_if_not_found or filepath else filename + return self.search_file( + filename, + ['', '.ly'], + datadirs=self.get_datadirs('img'), + ) 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 a874628c..4f33a084 100644 --- a/patacrep/songs/chordpro/__init__.py +++ b/patacrep/songs/chordpro/__init__.py @@ -1,9 +1,9 @@ """Chordpro parser""" -from jinja2 import Environment, FileSystemLoader, contextfunction +from jinja2 import Environment, FileSystemLoader, contextfunction, ChoiceLoader import jinja2 import os -import pkg_resources +from pkg_resources import resource_filename from patacrep import encoding, files from patacrep.songs import Song @@ -11,23 +11,10 @@ from patacrep.songs.chordpro.syntax import parse_song from patacrep.templates import Renderer class ChordproSong(Song): - """Chordpros song parser.""" + """Chordpro song parser""" + # pylint: disable=abstract-method - @staticmethod - def iter_template_paths(templatedirs, output_format): - """Iterate over paths in which templates are to be searched. - - :param iterator templatedirs: Iterators of additional directories (the - default hard-coded template directory is returned last). - :param str output_format: Song output format, which is appended to - each directory. - """ - for directory in templatedirs: - yield os.path.join(directory, output_format) - yield os.path.join( - os.path.abspath(pkg_resources.resource_filename(__name__, 'data')), - output_format, - ) + output_language = None def _parse(self, config): """Parse content, and return the dictionary of song data.""" @@ -41,10 +28,7 @@ class ChordproSong(Song): 'song': song, } - def render(self, output_format, output=None, template="song", templatedirs=None): # pylint: disable=arguments-differ - if templatedirs is None: - templatedirs = [] - + def render(self, output=None, template="song"): # pylint: disable=arguments-differ context = { 'language': self.languages[0], "titles": self.titles, @@ -55,9 +39,14 @@ class ChordproSong(Song): "content": self.cached['song'].content, } - jinjaenv = Environment(loader=FileSystemLoader( - self.iter_template_paths(templatedirs, output_format) - )) + jinjaenv = Environment(loader=ChoiceLoader([ + FileSystemLoader( + self.get_datadirs(os.path.join("templates", self.output_language)) + ), + FileSystemLoader( + os.path.join(resource_filename(__name__, 'data'), self.output_language) + ), + ])) jinjaenv.filters['search_image'] = self.search_image jinjaenv.filters['search_partition'] = self.search_partition @@ -68,7 +57,7 @@ class ChordproSong(Song): jinjaenv=jinjaenv, ).template.render(context) except jinja2.exceptions.TemplateNotFound: - raise NotImplementedError("Cannot convert to format '{}'.".format(output_format)) + raise NotImplementedError("Cannot convert to format '{}'.".format(self.output_language)) @staticmethod @contextfunction @@ -77,6 +66,51 @@ class ChordproSong(Song): context.vars['content'] = content return context.environment.get_template(content.template()).render(context) -SONG_PARSERS = { - 'sgc': ChordproSong, +class Chordpro2HtmlSong(ChordproSong): + """Render chordpro song to html code""" + + output_language = "html" + + def search_file(self, filename, extensions=None, *, datadirs=None): + try: + datadir, filename, extensions = self.search_datadir_file(filename, extensions, datadirs) + return os.path.join(datadir, filename + extensions) + except FileNotFoundError: + return None + +class Chordpro2LatexSong(ChordproSong): + """Render chordpro song to latex code""" + + output_language = "latex" + + def search_file(self, filename, extensions=None, *, datadirs=None): + try: + _datadir, filename, _extension = self.search_datadir_file( + filename, + extensions, + datadirs, + ) + return filename + except FileNotFoundError: + return None + +class Chordpro2ChordproSong(ChordproSong): + """Render chordpro song to chordpro code""" + + output_language = "chordpro" + + def search_file(self, filename, extensions=None, *, datadirs=None): + # pylint: disable=unused-variable + return filename + +SONG_RENDERERS = { + "latex": { + 'sgc': Chordpro2LatexSong, + }, + "html": { + 'sgc': Chordpro2HtmlSong, + }, + "chordpro": { + 'sgc': Chordpro2ChordproSong, + }, } diff --git a/patacrep/songs/chordpro/data/html/content_image b/patacrep/songs/chordpro/data/html/content_image index 0da4bcfe..bf46a95f 100644 --- a/patacrep/songs/chordpro/data/html/content_image +++ b/patacrep/songs/chordpro/data/html/content_image @@ -1 +1,6 @@ +(* block image *) +(* set image = content.argument|search_image *) +(* if image *) +(* endif *) +(* endblock *) diff --git a/patacrep/songs/chordpro/data/html/content_metadata_cover b/patacrep/songs/chordpro/data/html/content_metadata_cover index 96fc7718..0da23623 100644 --- a/patacrep/songs/chordpro/data/html/content_metadata_cover +++ b/patacrep/songs/chordpro/data/html/content_metadata_cover @@ -1,3 +1,8 @@ +(* block cov *) (* if 'cov' in metadata -*) -
-(* endif *) \ No newline at end of file + (* set cov = metadatad['cov'].argument|search_image *) + (* if cov *) +
+ (* endif *) +(* endif *) +(* endblock *) diff --git a/patacrep/songs/chordpro/data/html/content_partition b/patacrep/songs/chordpro/data/html/content_partition index 1075cced..b43318f8 100644 --- a/patacrep/songs/chordpro/data/html/content_partition +++ b/patacrep/songs/chordpro/data/html/content_partition @@ -1 +1,6 @@ -((content.argument)) +(* block partition *) +(* set partition = content.argument|search_partition *) +(* if partition *) +((content.argument)) +(* endif *) +(* endblock *) diff --git a/patacrep/songs/chordpro/data/latex/content_image b/patacrep/songs/chordpro/data/latex/content_image index e4c2befb..ac97404b 100644 --- a/patacrep/songs/chordpro/data/latex/content_image +++ b/patacrep/songs/chordpro/data/latex/content_image @@ -1 +1,6 @@ -\image{(( content.argument|search_image ))} +(* block image *) +(* set image = content.argument|search_image *) +(* if image *) +\image{(( image ))} +(*- endif *) +(*- endblock *) diff --git a/patacrep/songs/chordpro/data/latex/content_partition b/patacrep/songs/chordpro/data/latex/content_partition index 4ce134a1..a3c35a3e 100644 --- a/patacrep/songs/chordpro/data/latex/content_partition +++ b/patacrep/songs/chordpro/data/latex/content_partition @@ -1 +1,6 @@ +(* block partition *) +(* set partition = content.argument|search_partition *) +(* if partition *) \lilypond{ ((- content.argument|search_partition -)) } +(*- endif -*) +(*- endblock -*) diff --git a/patacrep/songs/chordpro/data/latex/song b/patacrep/songs/chordpro/data/latex/song index 6f51d673..050cf66b 100644 --- a/patacrep/songs/chordpro/data/latex/song +++ b/patacrep/songs/chordpro/data/latex/song @@ -28,7 +28,12 @@ (* endif *) (* endfor *) (* if 'cov' in metadata *) - cov={(( metadata["cov"].argument|search_image ))}, + (* block cov *) + (* set cov = metadata["cov"].argument|search_image *) + (* if cov *) + cov={(( cov ))}, + (* endif *) + (* endblock *) (* endif *) (* for key in metadata.morekeys *) (( key.keyword ))={(( key.argument ))}, diff --git a/patacrep/songs/convert/__main__.py b/patacrep/songs/convert/__main__.py index 51786875..6b40dca4 100644 --- a/patacrep/songs/convert/__main__.py +++ b/patacrep/songs/convert/__main__.py @@ -36,22 +36,29 @@ if __name__ == "__main__": dest = sys.argv[2] song_files = sys.argv[3:] - song_parsers = files.load_plugins( + renderers = files.load_plugins( datadirs=DEFAULT_CONFIG.get('datadir', []), root_modules=['songs'], - keyword='SONG_PARSERS', + keyword='SONG_RENDERERS', ) - if source not in song_parsers: + if dest not in renderers: LOGGER.error( - "Unknown file format '%s'. Available ones are %s.", + "Unknown destination file format '%s'. Available ones are %s.", source, - ", ".join(["'{}'".format(key) for key in song_parsers.keys()]) + ", ".join(["'{}'".format(key) for key in renderers.keys()]) + ) + sys.exit(1) + if source not in renderers[dest]: + LOGGER.error( + "Unknown source file format '%s'. Available ones are %s.", + source, + ", ".join(["'{}'".format(key) for key in renderers[dest].keys()]) ) sys.exit(1) for file in song_files: - song = song_parsers[source](file, DEFAULT_CONFIG) + song = renderers[dest][source](file, DEFAULT_CONFIG) try: destname = "{}.{}".format(".".join(file.split(".")[:-1]), dest) if os.path.exists(destname): diff --git a/patacrep/songs/latex/__init__.py b/patacrep/songs/latex/__init__.py index 6ac54c81..565b4067 100644 --- a/patacrep/songs/latex/__init__.py +++ b/patacrep/songs/latex/__init__.py @@ -11,8 +11,9 @@ from patacrep import files, encoding from patacrep.latex import parse_song from patacrep.songs import Song -class LatexSong(Song): - """LaTeX song parser.""" +class Latex2LatexSong(Song): + """Song written in LaTeX, rendered in LaTeX""" + # pylint: disable=abstract-method def _parse(self, __config): """Parse content, and return the dictinory of song data.""" @@ -28,8 +29,9 @@ class LatexSong(Song): else: self.authors = [] - def render_latex(self, output): + def render(self, output): """Return the code rendering the song.""" + # pylint: disable=signature-differs if output is None: raise ValueError(output) path = files.path2posix(files.relpath( @@ -38,7 +40,9 @@ class LatexSong(Song): )) return r'\import{{{}/}}{{{}}}'.format(os.path.dirname(path), os.path.basename(path)) -SONG_PARSERS = { - 'is': LatexSong, - 'sg': LatexSong, - } +SONG_RENDERERS = { + "latex": { + 'is': Latex2LatexSong, + 'sg': Latex2LatexSong, + }, +} diff --git a/test/test_chordpro/datadir/img/greensleeves.ly b/test/test_chordpro/datadir/img/greensleeves.ly new file mode 100644 index 00000000..e69de29b diff --git a/test/test_chordpro/datadir/img/traditionnel.png b/test/test_chordpro/datadir/img/traditionnel.png new file mode 100644 index 00000000..e69de29b diff --git a/test/test_chordpro/greensleeves.tex b/test/test_chordpro/greensleeves.tex index a0b602c0..b7186121 100644 --- a/test/test_chordpro/greensleeves.tex +++ b/test/test_chordpro/greensleeves.tex @@ -7,7 +7,7 @@ Un sous titre}[ by={ Traditionnel }, album={Angleterre}, - cov={traditionnel}, + cov={traditionnel}, ] \cover diff --git a/test/test_chordpro/metadata.sgc b/test/test_chordpro/metadata.sgc index 979c1388..e270b729 100644 --- a/test/test_chordpro/metadata.sgc +++ b/test/test_chordpro/metadata.sgc @@ -10,13 +10,13 @@ {artist: Author2} {album: Album} {copyright: Copyright} -{cov: Cover} +{cov: metadata_cover} {key: foo: Foo} {comment: Comment} {guitar_comment: GuitarComment} -{partition: Lilypond} -{image: Image} +{partition: metadata_lilypond} +{image: metadata_image} Foo diff --git a/test/test_chordpro/metadata.source b/test/test_chordpro/metadata.source index 65a2359a..05031dd2 100644 --- a/test/test_chordpro/metadata.source +++ b/test/test_chordpro/metadata.source @@ -10,12 +10,12 @@ {artist: Author2} {album: Album} {copyright: Copyright} -{cover: Cover} +{cover: metadata_cover} {capo: Capo} {key: foo: Foo} {comment: Comment} {guitar_comment: GuitarComment} -{partition: Lilypond} -{image: Image} +{partition: metadata_lilypond} +{image: metadata_image} Foo diff --git a/test/test_chordpro/metadata.tex b/test/test_chordpro/metadata.tex index fa29ec3c..46368580 100644 --- a/test/test_chordpro/metadata.tex +++ b/test/test_chordpro/metadata.tex @@ -11,7 +11,7 @@ Subtitle5}[ Author2 }, album={Album}, copyright={Copyright}, - cov={Cover}, + cov={test/test_chordpro/metadata_cover}, foo={Foo}, ] @@ -19,8 +19,8 @@ Subtitle5}[ \textnote{Comment} \musicnote{GuitarComment} -\lilypond{Lilypond} -\image{Image} +\lilypond{test/test_chordpro/metadata_lilypond} +\image{test/test_chordpro/metadata_image} diff --git a/test/test_chordpro/metadata_cover.png b/test/test_chordpro/metadata_cover.png new file mode 100644 index 00000000..e69de29b diff --git a/test/test_chordpro/metadata_image.png b/test/test_chordpro/metadata_image.png new file mode 100644 index 00000000..e69de29b diff --git a/test/test_chordpro/metadata_lilypond.ly b/test/test_chordpro/metadata_lilypond.ly new file mode 100644 index 00000000..e69de29b diff --git a/test/test_chordpro/test_parser.py b/test/test_chordpro/test_parser.py index 20cafd38..6ce2a83a 100644 --- a/test/test_chordpro/test_parser.py +++ b/test/test_chordpro/test_parser.py @@ -5,9 +5,10 @@ import glob import os import unittest +from pkg_resources import resource_filename +from patacrep import files from patacrep.build import DEFAULT_CONFIG -from patacrep.songs.chordpro import ChordproSong from .. import disable_logging @@ -26,11 +27,22 @@ class FileTestMeta(type): def __init__(cls, name, bases, nmspc): super().__init__(name, bases, nmspc) + # Setting datadir + cls.config = DEFAULT_CONFIG + if 'datadir' not in cls.config: + cls.config['datadir'] = [] + cls.config['datadir'].append(resource_filename(__name__, 'datadir')) + + cls.song_plugins = files.load_plugins( + datadirs=cls.config['datadir'], + root_modules=['songs'], + keyword='SONG_RENDERERS', + ) for source in sorted(glob.glob(os.path.join( os.path.dirname(__file__), '*.source', ))): - base = source[:-len(".source")] + base = os.path.relpath(source, os.getcwd())[:-len(".source")] for dest in LANGUAGES: destname = "{}.{}".format(base, dest) if not os.path.exists(destname): @@ -55,9 +67,8 @@ class FileTestMeta(type): chordproname = "{}.source".format(base) with disable_logging(): self.assertMultiLineEqual( - ChordproSong(chordproname, DEFAULT_CONFIG).render( + self.song_plugins[LANGUAGES[dest]]['sgc'](chordproname, self.config).render( output=chordproname, - output_format=LANGUAGES[dest], ).strip(), expectfile.read().strip(), )