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(
datadirs=config.get('datadir', []),
root_modules=['songs'],
keyword='SONG_PARSERS',
)
keyword='SONG_RENDERERS',
)['latex']
# Configuration set
config['render'] = content.render

2
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

14
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

62
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).

90
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,
},
}

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 ))">
(* endif *)
(* endblock *)

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

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

7
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 ))},

19
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):

18
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,
},
}

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={
Traditionnel },
album={Angleterre},
cov={traditionnel},
cov={traditionnel},
]
\cover

6
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

6
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

6
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}

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 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(),
)

Loading…
Cancel
Save