Browse Source

Create one Song class per couple parser/renderer

Instead of one song class per parser, which renders several formats
pull/122/head
Louis 9 years ago
parent
commit
51d4562901
  1. 4
      patacrep/build.py
  2. 2
      patacrep/content/song.py
  3. 14
      patacrep/files.py
  4. 62
      patacrep/songs/__init__.py
  5. 90
      patacrep/songs/chordpro/__init__.py
  6. 5
      patacrep/songs/chordpro/data/html/content_image
  7. 9
      patacrep/songs/chordpro/data/html/content_metadata_cover
  8. 7
      patacrep/songs/chordpro/data/html/content_partition
  9. 7
      patacrep/songs/chordpro/data/latex/content_image
  10. 5
      patacrep/songs/chordpro/data/latex/content_partition
  11. 7
      patacrep/songs/chordpro/data/latex/song
  12. 19
      patacrep/songs/convert/__main__.py
  13. 18
      patacrep/songs/latex/__init__.py
  14. 0
      test/test_chordpro/datadir/img/greensleeves.ly
  15. 0
      test/test_chordpro/datadir/img/traditionnel.png
  16. 2
      test/test_chordpro/greensleeves.tex
  17. 6
      test/test_chordpro/metadata.sgc
  18. 6
      test/test_chordpro/metadata.source
  19. 6
      test/test_chordpro/metadata.tex
  20. 0
      test/test_chordpro/metadata_cover.png
  21. 0
      test/test_chordpro/metadata_image.png
  22. 0
      test/test_chordpro/metadata_lilypond.ly
  23. 19
      test/test_chordpro/test_parser.py

4
patacrep/build.py

@ -110,8 +110,8 @@ class Songbook(object):
config['_song_plugins'] = files.load_plugins( config['_song_plugins'] = files.load_plugins(
datadirs=config.get('datadir', []), datadirs=config.get('datadir', []),
root_modules=['songs'], root_modules=['songs'],
keyword='SONG_PARSERS', keyword='SONG_RENDERERS',
) )['latex']
# Configuration set # Configuration set
config['render'] = content.render config['render'] = content.render

2
patacrep/content/song.py

@ -43,7 +43,7 @@ class SongRenderer(Content):
""").format( """).format(
separator="%"*80, separator="%"*80,
path=self.song.subpath, 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 #pylint: disable=unused-argument

14
patacrep/files.py

@ -8,6 +8,8 @@ import posixpath
import re import re
import sys import sys
from patacrep import utils
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
def recursive_find(root_directory, extensions): def recursive_find(root_directory, extensions):
@ -104,7 +106,7 @@ def load_plugins(datadirs, root_modules, keyword):
- keys are the keywords ; - keys are the keywords ;
- values are functions triggered when this keyword is met. - values are functions triggered when this keyword is met.
""" """
plugins = {} plugins = utils.DictOfDict()
datadir_path = [ datadir_path = [
os.path.join(datadir, "python", *root_modules) os.path.join(datadir, "python", *root_modules)
@ -121,13 +123,5 @@ def load_plugins(datadirs, root_modules, keyword):
prefix="patacrep.{}.".format(".".join(root_modules)) prefix="patacrep.{}.".format(".".join(root_modules))
): ):
if hasattr(module, keyword): if hasattr(module, keyword):
for (key, value) in getattr(module, keyword).items(): plugins.update(getattr(module, keyword))
if key in plugins:
LOGGER.warning(
"File %s: Keyword '%s' is already used. Ignored.",
module.__file__,
key,
)
continue
plugins[key] = value
return plugins return plugins

62
patacrep/songs/__init__.py

@ -167,16 +167,12 @@ class Song:
def __repr__(self): def __repr__(self):
return repr((self.titles, self.data, self.fullpath)) 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. """Return the code rendering this song.
Arguments: Arguments:
- output_format: Format of the output file (latex, chordpro...)
- output: Name of the output file, or `None` if irrelevant. - 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() raise NotImplementedError()
def _parse(self, config): # pylint: disable=no-self-use def _parse(self, config): # pylint: disable=no-self-use
@ -204,15 +200,24 @@ class Song:
if os.path.isdir(fullpath): if os.path.isdir(fullpath):
yield 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. """Search for a file name.
:param str filename: The name, as provided in the chordpro file (with or without extension). :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 list extensions: Possible extensions (with '.'). Default is no extension.
:param iterator directories: Other directories where to search for the file :param iterator directories: Other directories where to search for the file
The directory where the Song file is stored is added to the list. The directory where the Song file is stored is added to the list.
:return: A tuple `(datadir, filename, extension)` if file has been
Returns None if nothing found. 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 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 instance, it can compile a file, place it in a temporary folder, and
@ -223,28 +228,43 @@ class Song:
if directories is None: if directories is None:
directories = self.config['datadir'] 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) 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): raise FileNotFoundError(filename)
for extension in extensions:
fullpath = os.path.join(directory, filename + extension) def search_file(self, filename, extensions=None, *, datadirs=None):
if os.path.isfile(fullpath): """Return the path to a file present in a datadir.
return os.path.abspath(fullpath)
return None
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""" """Search for an image file"""
filepath = self.search_file( return self.search_file(
filename, filename,
['', '.jpg', '.png'], ['', '.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""" """Search for a lilypond file"""
filepath = self.search_file(filename, ['', '.ly']) return self.search_file(
return filepath if none_if_not_found or filepath else filename filename,
['', '.ly'],
datadirs=self.get_datadirs('img'),
)
def unprefixed_title(title, prefixes): def unprefixed_title(title, prefixes):
"""Remove the first prefix of the list in the beginning of title (if any). """Remove the first prefix of the list in the beginning of title (if any).

90
patacrep/songs/chordpro/__init__.py

@ -1,9 +1,9 @@
"""Chordpro parser""" """Chordpro parser"""
from jinja2 import Environment, FileSystemLoader, contextfunction from jinja2 import Environment, FileSystemLoader, contextfunction, ChoiceLoader
import jinja2 import jinja2
import os import os
import pkg_resources from pkg_resources import resource_filename
from patacrep import encoding, files from patacrep import encoding, files
from patacrep.songs import Song from patacrep.songs import Song
@ -11,23 +11,10 @@ from patacrep.songs.chordpro.syntax import parse_song
from patacrep.templates import Renderer from patacrep.templates import Renderer
class ChordproSong(Song): class ChordproSong(Song):
"""Chordpros song parser.""" """Chordpro song parser"""
# pylint: disable=abstract-method
@staticmethod output_language = None
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,
)
def _parse(self, config): def _parse(self, config):
"""Parse content, and return the dictionary of song data.""" """Parse content, and return the dictionary of song data."""
@ -41,10 +28,7 @@ class ChordproSong(Song):
'song': song, 'song': song,
} }
def render(self, output_format, output=None, template="song", templatedirs=None): # pylint: disable=arguments-differ def render(self, output=None, template="song"): # pylint: disable=arguments-differ
if templatedirs is None:
templatedirs = []
context = { context = {
'language': self.languages[0], 'language': self.languages[0],
"titles": self.titles, "titles": self.titles,
@ -55,9 +39,14 @@ class ChordproSong(Song):
"content": self.cached['song'].content, "content": self.cached['song'].content,
} }
jinjaenv = Environment(loader=FileSystemLoader( jinjaenv = Environment(loader=ChoiceLoader([
self.iter_template_paths(templatedirs, output_format) 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_image'] = self.search_image
jinjaenv.filters['search_partition'] = self.search_partition jinjaenv.filters['search_partition'] = self.search_partition
@ -68,7 +57,7 @@ class ChordproSong(Song):
jinjaenv=jinjaenv, jinjaenv=jinjaenv,
).template.render(context) ).template.render(context)
except jinja2.exceptions.TemplateNotFound: except jinja2.exceptions.TemplateNotFound:
raise NotImplementedError("Cannot convert to format '{}'.".format(output_format)) raise NotImplementedError("Cannot convert to format '{}'.".format(self.output_language))
@staticmethod @staticmethod
@contextfunction @contextfunction
@ -77,6 +66,51 @@ class ChordproSong(Song):
context.vars['content'] = content context.vars['content'] = content
return context.environment.get_template(content.template()).render(context) return context.environment.get_template(content.template()).render(context)
SONG_PARSERS = { class Chordpro2HtmlSong(ChordproSong):
'sgc': 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,
},
} }

5
patacrep/songs/chordpro/data/html/content_image

@ -1 +1,6 @@
(* block image *)
(* set image = content.argument|search_image *)
(* if image *)
<img src="(( content.argument|search_image ))"> <img src="(( content.argument|search_image ))">
(* endif *)
(* endblock *)

9
patacrep/songs/chordpro/data/html/content_metadata_cover

@ -1,3 +1,8 @@
(* block cov *)
(* if 'cov' in metadata -*) (* if 'cov' in metadata -*)
<img src="(( metadata['cov'].argument|search_image ))"><br> (* set cov = metadatad['cov'].argument|search_image *)
(* endif *) (* if cov *)
<img src="(( cov ))"><br>
(* endif *)
(* endif *)
(* endblock *)

7
patacrep/songs/chordpro/data/html/content_partition

@ -1 +1,6 @@
<a class="song-partition" href="(( content.argument|search_partition ))">((content.argument))</a> (* block partition *)
(* set partition = content.argument|search_partition *)
(* if partition *)
<a class="song-partition" href="(( partition ))">((content.argument))</a>
(* endif *)
(* endblock *)

7
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 *)

5
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 -)) } \lilypond{ ((- content.argument|search_partition -)) }
(*- endif -*)
(*- endblock -*)

7
patacrep/songs/chordpro/data/latex/song

@ -28,7 +28,12 @@
(* endif *) (* endif *)
(* endfor *) (* endfor *)
(* if 'cov' in metadata *) (* 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 *) (* endif *)
(* for key in metadata.morekeys *) (* for key in metadata.morekeys *)
(( key.keyword ))={(( key.argument ))}, (( key.keyword ))={(( key.argument ))},

19
patacrep/songs/convert/__main__.py

@ -36,22 +36,29 @@ if __name__ == "__main__":
dest = sys.argv[2] dest = sys.argv[2]
song_files = sys.argv[3:] song_files = sys.argv[3:]
song_parsers = files.load_plugins( renderers = files.load_plugins(
datadirs=DEFAULT_CONFIG.get('datadir', []), datadirs=DEFAULT_CONFIG.get('datadir', []),
root_modules=['songs'], root_modules=['songs'],
keyword='SONG_PARSERS', keyword='SONG_RENDERERS',
) )
if source not in song_parsers: if dest not in renderers:
LOGGER.error( LOGGER.error(
"Unknown file format '%s'. Available ones are %s.", "Unknown destination file format '%s'. Available ones are %s.",
source, 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) sys.exit(1)
for file in song_files: for file in song_files:
song = song_parsers[source](file, DEFAULT_CONFIG) song = renderers[dest][source](file, DEFAULT_CONFIG)
try: try:
destname = "{}.{}".format(".".join(file.split(".")[:-1]), dest) destname = "{}.{}".format(".".join(file.split(".")[:-1]), dest)
if os.path.exists(destname): if os.path.exists(destname):

18
patacrep/songs/latex/__init__.py

@ -11,8 +11,9 @@ from patacrep import files, encoding
from patacrep.latex import parse_song from patacrep.latex import parse_song
from patacrep.songs import Song from patacrep.songs import Song
class LatexSong(Song): class Latex2LatexSong(Song):
"""LaTeX song parser.""" """Song written in LaTeX, rendered in LaTeX"""
# pylint: disable=abstract-method
def _parse(self, __config): def _parse(self, __config):
"""Parse content, and return the dictinory of song data.""" """Parse content, and return the dictinory of song data."""
@ -28,8 +29,9 @@ class LatexSong(Song):
else: else:
self.authors = [] self.authors = []
def render_latex(self, output): def render(self, output):
"""Return the code rendering the song.""" """Return the code rendering the song."""
# pylint: disable=signature-differs
if output is None: if output is None:
raise ValueError(output) raise ValueError(output)
path = files.path2posix(files.relpath( path = files.path2posix(files.relpath(
@ -38,7 +40,9 @@ class LatexSong(Song):
)) ))
return r'\import{{{}/}}{{{}}}'.format(os.path.dirname(path), os.path.basename(path)) return r'\import{{{}/}}{{{}}}'.format(os.path.dirname(path), os.path.basename(path))
SONG_PARSERS = { SONG_RENDERERS = {
'is': LatexSong, "latex": {
'sg': LatexSong, 'is': Latex2LatexSong,
} 'sg': Latex2LatexSong,
},
}

0
test/test_chordpro/datadir/img/greensleeves.ly

0
test/test_chordpro/datadir/img/traditionnel.png

2
test/test_chordpro/greensleeves.tex

@ -7,7 +7,7 @@ Un sous titre}[
by={ by={
Traditionnel }, Traditionnel },
album={Angleterre}, album={Angleterre},
cov={traditionnel}, cov={traditionnel},
] ]
\cover \cover

6
test/test_chordpro/metadata.sgc

@ -10,13 +10,13 @@
{artist: Author2} {artist: Author2}
{album: Album} {album: Album}
{copyright: Copyright} {copyright: Copyright}
{cov: Cover} {cov: metadata_cover}
{key: foo: Foo} {key: foo: Foo}
{comment: Comment} {comment: Comment}
{guitar_comment: GuitarComment} {guitar_comment: GuitarComment}
{partition: Lilypond} {partition: metadata_lilypond}
{image: Image} {image: metadata_image}
Foo Foo

6
test/test_chordpro/metadata.source

@ -10,12 +10,12 @@
{artist: Author2} {artist: Author2}
{album: Album} {album: Album}
{copyright: Copyright} {copyright: Copyright}
{cover: Cover} {cover: metadata_cover}
{capo: Capo} {capo: Capo}
{key: foo: Foo} {key: foo: Foo}
{comment: Comment} {comment: Comment}
{guitar_comment: GuitarComment} {guitar_comment: GuitarComment}
{partition: Lilypond} {partition: metadata_lilypond}
{image: Image} {image: metadata_image}
Foo Foo

6
test/test_chordpro/metadata.tex

@ -11,7 +11,7 @@ Subtitle5}[
Author2 }, Author2 },
album={Album}, album={Album},
copyright={Copyright}, copyright={Copyright},
cov={Cover}, cov={test/test_chordpro/metadata_cover},
foo={Foo}, foo={Foo},
] ]
@ -19,8 +19,8 @@ Subtitle5}[
\textnote{Comment} \textnote{Comment}
\musicnote{GuitarComment} \musicnote{GuitarComment}
\lilypond{Lilypond} \lilypond{test/test_chordpro/metadata_lilypond}
\image{Image} \image{test/test_chordpro/metadata_image}

0
test/test_chordpro/metadata_cover.png

0
test/test_chordpro/metadata_image.png

0
test/test_chordpro/metadata_lilypond.ly

19
test/test_chordpro/test_parser.py

@ -5,9 +5,10 @@
import glob import glob
import os import os
import unittest import unittest
from pkg_resources import resource_filename
from patacrep import files
from patacrep.build import DEFAULT_CONFIG from patacrep.build import DEFAULT_CONFIG
from patacrep.songs.chordpro import ChordproSong
from .. import disable_logging from .. import disable_logging
@ -26,11 +27,22 @@ class FileTestMeta(type):
def __init__(cls, name, bases, nmspc): def __init__(cls, name, bases, nmspc):
super().__init__(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( for source in sorted(glob.glob(os.path.join(
os.path.dirname(__file__), os.path.dirname(__file__),
'*.source', '*.source',
))): ))):
base = source[:-len(".source")] base = os.path.relpath(source, os.getcwd())[:-len(".source")]
for dest in LANGUAGES: for dest in LANGUAGES:
destname = "{}.{}".format(base, dest) destname = "{}.{}".format(base, dest)
if not os.path.exists(destname): if not os.path.exists(destname):
@ -55,9 +67,8 @@ class FileTestMeta(type):
chordproname = "{}.source".format(base) chordproname = "{}.source".format(base)
with disable_logging(): with disable_logging():
self.assertMultiLineEqual( self.assertMultiLineEqual(
ChordproSong(chordproname, DEFAULT_CONFIG).render( self.song_plugins[LANGUAGES[dest]]['sgc'](chordproname, self.config).render(
output=chordproname, output=chordproname,
output_format=LANGUAGES[dest],
).strip(), ).strip(),
expectfile.read().strip(), expectfile.read().strip(),
) )

Loading…
Cancel
Save