From bcac69b8cb0b5985845cc4c5f3e3d51c86ed4bd7 Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 24 May 2014 11:40:59 +0200 Subject: [PATCH 01/27] WIP: Draft of content plugin management --- songbook_core/content/__init__.py | 0 songbook_core/content/example | 70 +++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 songbook_core/content/__init__.py create mode 100644 songbook_core/content/example diff --git a/songbook_core/content/__init__.py b/songbook_core/content/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/songbook_core/content/example b/songbook_core/content/example new file mode 100644 index 00000000..190cc997 --- /dev/null +++ b/songbook_core/content/example @@ -0,0 +1,70 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""Example of a plugin managing a content type. + +TODO: Explain""" + +import songbook.content + +class Example(songbook.content.Content): + """Content item of type 'example'.""" + + def __init__(self): + """Initialization. + + No signature is imposed.""" + pass + + def render(self, TODO): + """Render this content item. + + Returns a string, to be placed verbatim (? TODO) in the generated .tex + file. + """ + return "" + + # Block management + + def begin_new_block(self, previous): + """Return a boolean stating if a new block is to be created. + + # Arguments + + - previous: the songbook.content.Content object of the previous item. + + # Return + + True if the renderer has to close previous block, and begin a new one, + False otherwise. + + # Default + + The default behavior of this method (if not defined in this child + class) is: begin a new block if the previous item is not an instance of + the same class. + """ + return False + + def begin_block(self, TODO): + """Return the string to begin a block.""" + return "" + + def end_block(self, TODO): + """Return the string to end a block.""" + + +def parse(keyword, arguments): + """Parse songbook .sb content item. + + Takes as argument the keyword triggerring this content, and the list of + arguments, e.g. for a content item '["picture", "*.png", "*.jpg"]', the + call of this function is: + + > parse('picture', ["*.png", "*.jpg"]) + + Return a list of (subclasses of) songbook.content.Content objects. + """ + return Example(keyword, arguments) + +songbook.content.register('example', parse) From a5fa34fe20b3ae5cd6923ef5caba44aeeccccdb1 Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 24 May 2014 16:32:49 +0200 Subject: [PATCH 02/27] corrections --- songbook_core/content/example | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/songbook_core/content/example b/songbook_core/content/example index 190cc997..061fe599 100644 --- a/songbook_core/content/example +++ b/songbook_core/content/example @@ -10,12 +10,6 @@ import songbook.content class Example(songbook.content.Content): """Content item of type 'example'.""" - def __init__(self): - """Initialization. - - No signature is imposed.""" - pass - def render(self, TODO): """Render this content item. @@ -52,6 +46,7 @@ class Example(songbook.content.Content): def end_block(self, TODO): """Return the string to end a block.""" + return "" def parse(keyword, arguments): From 7c4ca25ed109b22df28453ca93109adb1835d2ae Mon Sep 17 00:00:00 2001 From: Louis Date: Fri, 13 Jun 2014 16:59:42 +0200 Subject: [PATCH 03/27] [WIP] Draft of section and songsection plugins --- songbook_core/content/__init__.py | 79 ++++++++++++++++++++++++++++ songbook_core/content/example | 65 ----------------------- songbook_core/content/section.py | 37 +++++++++++++ songbook_core/content/songsection.py | 25 +++++++++ 4 files changed, 141 insertions(+), 65 deletions(-) delete mode 100644 songbook_core/content/example create mode 100644 songbook_core/content/section.py create mode 100644 songbook_core/content/songsection.py diff --git a/songbook_core/content/__init__.py b/songbook_core/content/__init__.py index e69de29b..68323ee8 100644 --- a/songbook_core/content/__init__.py +++ b/songbook_core/content/__init__.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import glob +import importlib +import logging +import os + +from songbook_core.errors import SongbookError + +LOGGER = logging.getLogger(__name__) + +class Content: + """Content item of type 'example'.""" + + def render(self): + """Render this content item. + + Returns a string, to be placed verbatim in the generated .tex file. + """ + return "" + + # Block management + + def begin_new_block(self, previous): + """Return a boolean stating if a new block is to be created. + + # Arguments + + - previous: the songbook.content.Content object of the previous item. + + # Return + + True if the renderer has to close previous block, and begin a new one, + False otherwise. + + # Default + + The default behavior of this method (if not defined in this child + class) is: begin a new block if the previous item is not an instance of + the same class. + """ + return False + + def begin_block(self): + """Return the string to begin a block.""" + return "" + + def end_block(self): + """Return the string to end a block.""" + return "" + +class ContentError(SongbookError): + def __init__(self, keyword, message): + self.keyword = keyword + self.message = message + + def __str__(self): + return "Content: {}: {}".format(self.keyword, self.message) + +def load_plugins(): + """Load all content plugins, and return a dictionary of those plugins. + + Return value: a dictionary where: + - keys are the keywords ; + - values are functions triggered when this keyword is met. + """ + plugins = {} + for name in glob.glob(os.path.join(os.path.dirname(__file__), "*.py")): + if name.endswith(".py") and os.path.basename(name) != "__init__.py": + plugin = importlib.import_module( + 'songbook_core.content.{}'.format(os.path.basename(name[:-len('.py')])) + ) + for (key, value) in plugin.CONTENT_PLUGINS.items(): + if key in plugins: + LOGGER.warning("File %s: Keyword '%s' is already used. Ignored.", os.path.relpath(name), key) + continue + plugins[key] = value + return plugins diff --git a/songbook_core/content/example b/songbook_core/content/example deleted file mode 100644 index 061fe599..00000000 --- a/songbook_core/content/example +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -"""Example of a plugin managing a content type. - -TODO: Explain""" - -import songbook.content - -class Example(songbook.content.Content): - """Content item of type 'example'.""" - - def render(self, TODO): - """Render this content item. - - Returns a string, to be placed verbatim (? TODO) in the generated .tex - file. - """ - return "" - - # Block management - - def begin_new_block(self, previous): - """Return a boolean stating if a new block is to be created. - - # Arguments - - - previous: the songbook.content.Content object of the previous item. - - # Return - - True if the renderer has to close previous block, and begin a new one, - False otherwise. - - # Default - - The default behavior of this method (if not defined in this child - class) is: begin a new block if the previous item is not an instance of - the same class. - """ - return False - - def begin_block(self, TODO): - """Return the string to begin a block.""" - return "" - - def end_block(self, TODO): - """Return the string to end a block.""" - return "" - - -def parse(keyword, arguments): - """Parse songbook .sb content item. - - Takes as argument the keyword triggerring this content, and the list of - arguments, e.g. for a content item '["picture", "*.png", "*.jpg"]', the - call of this function is: - - > parse('picture', ["*.png", "*.jpg"]) - - Return a list of (subclasses of) songbook.content.Content objects. - """ - return Example(keyword, arguments) - -songbook.content.register('example', parse) diff --git a/songbook_core/content/section.py b/songbook_core/content/section.py new file mode 100644 index 00000000..99f66ef7 --- /dev/null +++ b/songbook_core/content/section.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from songbook_core.content import Content + +KEYWORDS = [ + "part", + "chapter", + "section", + "subsection", + "subsubsection", + "paragraph", + "subparagraph", + ] +FULL_KEYWORDS = KEYWORDS + [ "{}*".format(keyword) for keyword in KEYWORDS] + +class Section(Content): + def __init__(self, keyword, name, short = None): + self.keyword = keyword + self.name = name + self.short = short + + def render(self): + if (short is None): + return r'\{}{{{}}}'.format(self.keyword, self.name) + else: + return r'\{}[{}]{{{}}}'.format(self.keyword, self.short, self.name) + +def parse(keyword, arguments): + if (keyword not in KEYWORDS) and (len(arguments) != 1): + raise ContentError(keyword, "Starred section names must have exactly one argument.") + if (len(arguments) not in [1, 2]): + raise ContentError(keyword, "Section can have one or two arguments.") + return [Section(keyword, *arguments)] + + +CONTENT_PLUGINS = dict([(keyword, parse) for keyword in FULL_KEYWORDS]) diff --git a/songbook_core/content/songsection.py b/songbook_core/content/songsection.py new file mode 100644 index 00000000..0b246e65 --- /dev/null +++ b/songbook_core/content/songsection.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from songbook_core.content import Content + +KEYWORDS = [ + "songchapter", + "songsection", + ] + +class SongSection(Content): + def __init__(self, keyword): + self.keyword = keyword + self.name = name + + def render(self): + return r'\{}{{{}}}'.format(self.keyword, self.name) + +def parse(keyword, arguments): + if (keyword not in KEYWORDS) and (len(arguments) != 1): + raise ContentError(keyword, "Starred section names must have exactly one argument.") + return [SongSection(keyword, *arguments)] + + +CONTENT_PLUGINS = dict([(keyword, parse) for keyword in KEYWORDS]) From 3f9cdc6c8b78a0f47c998b17694d3b2d9ab7f72f Mon Sep 17 00:00:00 2001 From: Louis Date: Fri, 13 Jun 2014 17:52:34 +0200 Subject: [PATCH 04/27] [WIP] Plugins section and songsection work (no song yet) --- songbook_core/build.py | 42 ++++------------- songbook_core/content/__init__.py | 62 ++++++++++++++++++++++++++ songbook_core/content/section.py | 4 +- songbook_core/content/songsection.py | 4 +- songbook_core/data/templates/songs.tex | 32 +++++++------ 5 files changed, 92 insertions(+), 52 deletions(-) diff --git a/songbook_core/build.py b/songbook_core/build.py index 4101341b..496e7f9c 100755 --- a/songbook_core/build.py +++ b/songbook_core/build.py @@ -11,6 +11,7 @@ import re from subprocess import Popen, PIPE, call from songbook_core import __DATADIR__ +from songbook_core import content from songbook_core import errors from songbook_core.files import recursive_find from songbook_core.index import process_sxd @@ -66,6 +67,8 @@ class Songbook(object): Argument: - config : a dictionary containing the configuration + + TODO Move this function elsewhere """ Song.sort = config['sort'] if 'titleprefixwords' in config: @@ -94,34 +97,7 @@ class Songbook(object): self.config.update(raw_songbook) self._set_datadir() - # Compute song list - if self.config['content'] is None: - self.config['content'] = [( - "song", - os.path.relpath( - filename, - os.path.join(self.config['datadir'][0], 'songs'), - )) - for filename - in recursive_find( - os.path.join(self.config['datadir'][0], 'songs'), - '*.sg', - ) - ] - else: - content = self.config["content"] - self.config["content"] = [] - for elem in content: - if isinstance(elem, basestring): - self.config["content"].append(("song", elem)) - elif isinstance(elem, list): - self.config["content"].append((elem[0], elem[1])) - else: - raise errors.SBFileError( - "Syntax error: could not decode the content " - "of {0}".format(self.basename) - ) - + # TODO This should be moved elsewhere # Ensure self.config['authwords'] contains all entries for (key, value) in DEFAULT_AUTHWORDS.items(): if key not in self.config['authwords']: @@ -146,18 +122,15 @@ class Songbook(object): self.config['datadir'] = abs_datadir - def _parse_songs(self): - """Parse content included in songbook.""" - self.contentlist = SongbookContent(self.config['datadir']) - self.contentlist.append_list(self.config['content']) - def write_tex(self, output): """Build the '.tex' file corresponding to self. Arguments: - output: a file object, in which the file will be written. """ - self._parse_songs() + self.contentlist = content.process_content(self.config['content'], self.config) + #TODO self.contentlist = SongbookContent(self.config['datadir']) + #TODO self.contentlist.append_list(self.config['content']) renderer = TexRenderer( self.config['template'], self.config['datadir'], @@ -166,6 +139,7 @@ class Songbook(object): context = renderer.get_variables() context.update(self.config) + context['render_content'] = content.render_content context['titleprefixkeys'] = ["after", "sep", "ignore"] context['content'] = self.contentlist context['filename'] = output.name[:-4] diff --git a/songbook_core/content/__init__.py b/songbook_core/content/__init__.py index 68323ee8..a143be26 100644 --- a/songbook_core/content/__init__.py +++ b/songbook_core/content/__init__.py @@ -9,6 +9,7 @@ import os from songbook_core.errors import SongbookError LOGGER = logging.getLogger(__name__) +EOL = '\n' class Content: """Content item of type 'example'.""" @@ -77,3 +78,64 @@ def load_plugins(): continue plugins[key] = value return plugins + +def render_content(content): + rendered = "" + previous = None + last = None + for elem in content: + if not isinstance(elem, Content): + LOGGER.error("Ignoring bad content item '{}'.".format(elem)) + continue + + last = elem + if elem.begin_new_block(previous): + if previous: + rendered += previous.end_block() + EOL + rendered += elem.begin_block() + EOL + rendered += elem.render() + EOL + + if isinstance(last, Content): + rendered += last.end_block() + EOL + + return rendered + +def process_content(content, config = None): + contentlist = [] + plugins = load_plugins() + for elem in content: + if isinstance(elem, basestring): + TODO + if len(content) == 0: + TODO + if elem[0] not in plugins: + raise ContentError(elem[0], "Unknown content type.") + contentlist.extend(plugins[elem[0]](*elem)) + return contentlist + ## Compute song list + #if self.config['content'] is None: + # self.config['content'] = [( + # "song", + # os.path.relpath( + # filename, + # os.path.join(self.config['datadir'][0], 'songs'), + # )) + # for filename + # in recursive_find( + # os.path.join(self.config['datadir'][0], 'songs'), + # '*.sg', + # ) + # ] + #else: + # content = self.config["content"] + # self.config["content"] = [] + # for elem in content: + # if isinstance(elem, basestring): + # self.config["content"].append(("song", elem)) + # elif isinstance(elem, list): + # self.config["content"].append((elem[0], elem[1])) + # else: + # raise errors.SBFileError( + # "Syntax error: could not decode the content " + # "of {0}".format(self.basename) + # ) diff --git a/songbook_core/content/section.py b/songbook_core/content/section.py index 99f66ef7..7956c602 100644 --- a/songbook_core/content/section.py +++ b/songbook_core/content/section.py @@ -21,12 +21,12 @@ class Section(Content): self.short = short def render(self): - if (short is None): + if (self.short is None): return r'\{}{{{}}}'.format(self.keyword, self.name) else: return r'\{}[{}]{{{}}}'.format(self.keyword, self.short, self.name) -def parse(keyword, arguments): +def parse(keyword, *arguments): if (keyword not in KEYWORDS) and (len(arguments) != 1): raise ContentError(keyword, "Starred section names must have exactly one argument.") if (len(arguments) not in [1, 2]): diff --git a/songbook_core/content/songsection.py b/songbook_core/content/songsection.py index 0b246e65..7327696c 100644 --- a/songbook_core/content/songsection.py +++ b/songbook_core/content/songsection.py @@ -9,14 +9,14 @@ KEYWORDS = [ ] class SongSection(Content): - def __init__(self, keyword): + def __init__(self, keyword, name): self.keyword = keyword self.name = name def render(self): return r'\{}{{{}}}'.format(self.keyword, self.name) -def parse(keyword, arguments): +def parse(keyword, *arguments): if (keyword not in KEYWORDS) and (len(arguments) != 1): raise ContentError(keyword, "Starred section names must have exactly one argument.") return [SongSection(keyword, *arguments)] diff --git a/songbook_core/data/templates/songs.tex b/songbook_core/data/templates/songs.tex index 2b208f08..d3e6db17 100644 --- a/songbook_core/data/templates/songs.tex +++ b/songbook_core/data/templates/songs.tex @@ -70,9 +70,10 @@ (* block songbookpreambule *) (( super() )) - (* for lang in content.languages() *) - \PassOptionsToPackage{((lang))}{babel} - (* endfor *) + %!TODO + %!(* for lang in content.languages() *) + %!\PassOptionsToPackage{((lang))}{babel} + %!(* endfor *) \usepackage[((lang))]{babel} \lang{((lang))} @@ -93,15 +94,18 @@ \phantomsection \addcontentsline{toc}{section}{\songlistname} - \begin{songs}{((indexes|default('')))} - (* for type, elem in content.content *) - (* if type=="song" *) - \input{((elem.path))} - (* elif type=="section" *) - \end{songs} - \songsection{((elem))} - \begin{songs}{((indexes|default('')))} - (* endif *) - (* endfor *) - \end{songs} + %!TODO + %!\begin{songs}{((indexes|default('')))} + %! (* for type, elem in content.content *) + %! (* if type=="song" *) + %! \input{((elem.path))} + %! (* elif type=="section" *) + %! \end{songs} + %! \songsection{((elem))} + %! \begin{songs}{((indexes|default('')))} + %! (* endif *) + %! (* endfor *) + %!\end{songs} + (( render_content(content) )) + (* endblock *) From 2fbff31ca968c8396dfa7c0796ff072eb4780ee9 Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 14 Jun 2014 11:08:44 +0200 Subject: [PATCH 05/27] [WIP] Basic version of song importation work (but I do not think it compiles) --- songbook_core/build.py | 3 -- songbook_core/content/__init__.py | 42 +++---------------- songbook_core/content/section.py | 2 +- songbook_core/content/song.py | 57 ++++++++++++++++++++++++++ songbook_core/content/songsection.py | 2 +- songbook_core/data/templates/songs.tex | 14 +------ songbook_core/songs.py | 32 --------------- 7 files changed, 65 insertions(+), 87 deletions(-) create mode 100644 songbook_core/content/song.py diff --git a/songbook_core/build.py b/songbook_core/build.py index 496e7f9c..31f94444 100755 --- a/songbook_core/build.py +++ b/songbook_core/build.py @@ -13,7 +13,6 @@ from subprocess import Popen, PIPE, call from songbook_core import __DATADIR__ from songbook_core import content from songbook_core import errors -from songbook_core.files import recursive_find from songbook_core.index import process_sxd from songbook_core.songs import Song, SongbookContent from songbook_core.templates import TexRenderer @@ -129,8 +128,6 @@ class Songbook(object): - output: a file object, in which the file will be written. """ self.contentlist = content.process_content(self.config['content'], self.config) - #TODO self.contentlist = SongbookContent(self.config['datadir']) - #TODO self.contentlist.append_list(self.config['content']) renderer = TexRenderer( self.config['template'], self.config['datadir'], diff --git a/songbook_core/content/__init__.py b/songbook_core/content/__init__.py index a143be26..27c5967a 100644 --- a/songbook_core/content/__init__.py +++ b/songbook_core/content/__init__.py @@ -34,14 +34,8 @@ class Content: True if the renderer has to close previous block, and begin a new one, False otherwise. - - # Default - - The default behavior of this method (if not defined in this child - class) is: begin a new block if the previous item is not an instance of - the same class. """ - return False + return True def begin_block(self): """Return the string to begin a block.""" @@ -94,6 +88,7 @@ def render_content(content): rendered += previous.end_block() + EOL rendered += elem.begin_block() + EOL rendered += elem.render() + EOL + previous = elem if isinstance(last, Content): rendered += last.end_block() + EOL @@ -105,37 +100,10 @@ def process_content(content, config = None): plugins = load_plugins() for elem in content: if isinstance(elem, basestring): - TODO + elem = ["song", elem] if len(content) == 0: - TODO + content = ["song"] if elem[0] not in plugins: raise ContentError(elem[0], "Unknown content type.") - contentlist.extend(plugins[elem[0]](*elem)) + contentlist.extend(plugins[elem[0]](elem[0], config, *elem[1:])) return contentlist - ## Compute song list - #if self.config['content'] is None: - # self.config['content'] = [( - # "song", - # os.path.relpath( - # filename, - # os.path.join(self.config['datadir'][0], 'songs'), - # )) - # for filename - # in recursive_find( - # os.path.join(self.config['datadir'][0], 'songs'), - # '*.sg', - # ) - # ] - #else: - # content = self.config["content"] - # self.config["content"] = [] - # for elem in content: - # if isinstance(elem, basestring): - # self.config["content"].append(("song", elem)) - # elif isinstance(elem, list): - # self.config["content"].append((elem[0], elem[1])) - # else: - # raise errors.SBFileError( - # "Syntax error: could not decode the content " - # "of {0}".format(self.basename) - # ) diff --git a/songbook_core/content/section.py b/songbook_core/content/section.py index 7956c602..8fb19527 100644 --- a/songbook_core/content/section.py +++ b/songbook_core/content/section.py @@ -26,7 +26,7 @@ class Section(Content): else: return r'\{}[{}]{{{}}}'.format(self.keyword, self.short, self.name) -def parse(keyword, *arguments): +def parse(keyword, config, *arguments): if (keyword not in KEYWORDS) and (len(arguments) != 1): raise ContentError(keyword, "Starred section names must have exactly one argument.") if (len(arguments) not in [1, 2]): diff --git a/songbook_core/content/song.py b/songbook_core/content/song.py new file mode 100644 index 00000000..b9b13858 --- /dev/null +++ b/songbook_core/content/song.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import glob +import os + +from songbook_core.content import Content +from songbook_core.files import recursive_find + +class Song(Content): + def __init__(self, filename): + self.filename = filename + + def begin_new_block(self, previous): + return not isinstance(previous, Song) + + def begin_block(self): + #TODO index return r'\begin{songs}{((indexes|default("")))}' + return r'\begin{songs}{titleidx,authidx}' + + def end_block(self): + return r'\end{songs}' + + def render(self): + return r'\input{{{}}}'.format(self.filename) + +def parse(keyword, config, *arguments): + songlist = [] + if not arguments: + import ipdb; ipdb.set_trace() + arguments = [ + os.path.relpath( + filename, + os.path.join(config['datadir'][0], 'songs'), + ) + for filename + in recursive_find( + os.path.join(config['datadir'][0], 'songs'), + "*.sg" + ) + ] + for elem in arguments: + before = len(songlist) + for songdir in [os.path.join(d, 'songs') for d in config['datadir']]: + for filename in glob.iglob(os.path.join(songdir, elem)): + songlist.append(Song(filename)) + if len(songlist) > before: + break + if len(songlist) == before: + # No songs were added + LOGGER.warning( + "Expression '{}' did not match any file".format(elem) + ) + return songlist + + +CONTENT_PLUGINS = {'song': parse} diff --git a/songbook_core/content/songsection.py b/songbook_core/content/songsection.py index 7327696c..61d7b88d 100644 --- a/songbook_core/content/songsection.py +++ b/songbook_core/content/songsection.py @@ -16,7 +16,7 @@ class SongSection(Content): def render(self): return r'\{}{{{}}}'.format(self.keyword, self.name) -def parse(keyword, *arguments): +def parse(keyword, config, *arguments): if (keyword not in KEYWORDS) and (len(arguments) != 1): raise ContentError(keyword, "Starred section names must have exactly one argument.") return [SongSection(keyword, *arguments)] diff --git a/songbook_core/data/templates/songs.tex b/songbook_core/data/templates/songs.tex index d3e6db17..8c9b8066 100644 --- a/songbook_core/data/templates/songs.tex +++ b/songbook_core/data/templates/songs.tex @@ -94,18 +94,6 @@ \phantomsection \addcontentsline{toc}{section}{\songlistname} - %!TODO - %!\begin{songs}{((indexes|default('')))} - %! (* for type, elem in content.content *) - %! (* if type=="song" *) - %! \input{((elem.path))} - %! (* elif type=="section" *) - %! \end{songs} - %! \songsection{((elem))} - %! \begin{songs}{((indexes|default('')))} - %! (* endif *) - %! (* endfor *) - %!\end{songs} - (( render_content(content) )) + ((render_content(content) )) (* endblock *) diff --git a/songbook_core/songs.py b/songbook_core/songs.py index 2bd1fc2e..048e1ffa 100644 --- a/songbook_core/songs.py +++ b/songbook_core/songs.py @@ -107,35 +107,3 @@ class SongbookContent(object): data = parsetex(filename) song = Song(filename, data['languages'], data['titles'], data['args']) self.content.append(("song", song)) - - def append(self, type, value): - """ Append a generic element to the content list""" - self.content.append((type, value)) - - def append_list(self, contentlist): - """Ajoute une liste de chansons à la liste - - L'argument est une liste de chaînes, représentant des noms de fichiers - sous la forme d'expressions régulières destinées à être analysées avec - le module glob. - """ - for type, elem in contentlist: - if type == "song": - # Add all the songs matching the regex - before = len(self.content) - for songdir in self.songdirs: - for filename in glob.iglob(os.path.join(songdir, elem)): - self.append_song(filename) - if len(self.content) > before: - break - if len(self.content) == before: - # No songs were added - LOGGER.warning( - "Expression '{}' did not match any file".format(elem) - ) - else: - self.append(type, elem) - - def languages(self): - """Renvoie la liste des langues utilisées par les chansons""" - return set().union(*[set(song.languages) for type, song in self.content if type=="song"]) From 2f3d57b8a26d38ed8789e27a7d0c8f2e8137d6e0 Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 14 Jun 2014 11:32:46 +0200 Subject: [PATCH 06/27] [WIP] indexes work again --- songbook_core/content/__init__.py | 23 +++++++++++++---------- songbook_core/content/section.py | 2 +- songbook_core/content/song.py | 16 +++++++++------- songbook_core/content/songsection.py | 2 +- 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/songbook_core/content/__init__.py b/songbook_core/content/__init__.py index 27c5967a..3b908868 100644 --- a/songbook_core/content/__init__.py +++ b/songbook_core/content/__init__.py @@ -3,6 +3,7 @@ import glob import importlib +import jinja2 import logging import os @@ -14,7 +15,7 @@ EOL = '\n' class Content: """Content item of type 'example'.""" - def render(self): + def render(self, context): """Render this content item. Returns a string, to be placed verbatim in the generated .tex file. @@ -23,12 +24,13 @@ class Content: # Block management - def begin_new_block(self, previous): + def begin_new_block(self, previous, context): """Return a boolean stating if a new block is to be created. # Arguments - previous: the songbook.content.Content object of the previous item. + - context: TODO # Return @@ -37,11 +39,11 @@ class Content: """ return True - def begin_block(self): + def begin_block(self, context): """Return the string to begin a block.""" return "" - def end_block(self): + def end_block(self, context): """Return the string to end a block.""" return "" @@ -73,7 +75,8 @@ def load_plugins(): plugins[key] = value return plugins -def render_content(content): +@jinja2.contextfunction +def render_content(context, content): rendered = "" previous = None last = None @@ -83,15 +86,15 @@ def render_content(content): continue last = elem - if elem.begin_new_block(previous): + if elem.begin_new_block(previous, context): if previous: - rendered += previous.end_block() + EOL - rendered += elem.begin_block() + EOL - rendered += elem.render() + EOL + rendered += previous.end_block(context) + EOL + rendered += elem.begin_block(context) + EOL + rendered += elem.render(context) + EOL previous = elem if isinstance(last, Content): - rendered += last.end_block() + EOL + rendered += last.end_block(context) + EOL return rendered diff --git a/songbook_core/content/section.py b/songbook_core/content/section.py index 8fb19527..f2cf4e89 100644 --- a/songbook_core/content/section.py +++ b/songbook_core/content/section.py @@ -20,7 +20,7 @@ class Section(Content): self.name = name self.short = short - def render(self): + def render(self, __context): if (self.short is None): return r'\{}{{{}}}'.format(self.keyword, self.name) else: diff --git a/songbook_core/content/song.py b/songbook_core/content/song.py index b9b13858..447d20b3 100644 --- a/songbook_core/content/song.py +++ b/songbook_core/content/song.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- import glob +import jinja2 import os from songbook_core.content import Content @@ -11,23 +12,24 @@ class Song(Content): def __init__(self, filename): self.filename = filename - def begin_new_block(self, previous): + def begin_new_block(self, previous, __context): return not isinstance(previous, Song) - def begin_block(self): - #TODO index return r'\begin{songs}{((indexes|default("")))}' - return r'\begin{songs}{titleidx,authidx}' + def begin_block(self, context): + indexes = context.resolve("indexes") + if isinstance(indexes, jinja2.runtime.Undefined): + indexes = "" + return r'\begin{songs}{%s}' % indexes - def end_block(self): + def end_block(self, __context): return r'\end{songs}' - def render(self): + def render(self, __context): return r'\input{{{}}}'.format(self.filename) def parse(keyword, config, *arguments): songlist = [] if not arguments: - import ipdb; ipdb.set_trace() arguments = [ os.path.relpath( filename, diff --git a/songbook_core/content/songsection.py b/songbook_core/content/songsection.py index 61d7b88d..c27a3a31 100644 --- a/songbook_core/content/songsection.py +++ b/songbook_core/content/songsection.py @@ -13,7 +13,7 @@ class SongSection(Content): self.keyword = keyword self.name = name - def render(self): + def render(self, __context): return r'\{}{{{}}}'.format(self.keyword, self.name) def parse(keyword, config, *arguments): From 29b5878cbfb785815f1411d671d5fdb7f70f4145 Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 14 Jun 2014 12:00:16 +0200 Subject: [PATCH 07/27] [WIP] Songs are now parsed using PlasTeX --- songbook_core/build.py | 3 ++- songbook_core/content/song.py | 21 ++++++++++++------- songbook_core/songs.py | 39 +++++++---------------------------- 3 files changed, 24 insertions(+), 39 deletions(-) diff --git a/songbook_core/build.py b/songbook_core/build.py index 31f94444..cbef29a6 100755 --- a/songbook_core/build.py +++ b/songbook_core/build.py @@ -14,7 +14,7 @@ from songbook_core import __DATADIR__ from songbook_core import content from songbook_core import errors from songbook_core.index import process_sxd -from songbook_core.songs import Song, SongbookContent +from songbook_core.songs import Song from songbook_core.templates import TexRenderer LOGGER = logging.getLogger(__name__) @@ -69,6 +69,7 @@ class Songbook(object): TODO Move this function elsewhere """ + return Song.sort = config['sort'] if 'titleprefixwords' in config: Song.prefixes = config['titleprefixwords'] diff --git a/songbook_core/content/song.py b/songbook_core/content/song.py index 447d20b3..fc371eaf 100644 --- a/songbook_core/content/song.py +++ b/songbook_core/content/song.py @@ -3,17 +3,18 @@ import glob import jinja2 +import logging import os from songbook_core.content import Content from songbook_core.files import recursive_find +from songbook_core.songs import Song -class Song(Content): - def __init__(self, filename): - self.filename = filename +LOGGER = logging.getLogger(__name__) +class SongRenderer(Content, Song): def begin_new_block(self, previous, __context): - return not isinstance(previous, Song) + return not isinstance(previous, SongRenderer) def begin_block(self, context): indexes = context.resolve("indexes") @@ -24,8 +25,13 @@ class Song(Content): def end_block(self, __context): return r'\end{songs}' - def render(self, __context): - return r'\input{{{}}}'.format(self.filename) + def render(self, context): + outdir = os.path.dirname(context['filename']) + if os.path.abspath(self.path).startswith(os.path.abspath(outdir)): + path = os.path.relpath(self.path, outdir) + else: + path = os.path.abspath(self.path) + return r'\input{{{}}}'.format(path) def parse(keyword, config, *arguments): songlist = [] @@ -45,7 +51,8 @@ def parse(keyword, config, *arguments): before = len(songlist) for songdir in [os.path.join(d, 'songs') for d in config['datadir']]: for filename in glob.iglob(os.path.join(songdir, elem)): - songlist.append(Song(filename)) + LOGGER.debug('Parsing file "{}"…'.format(filename)) + songlist.append(SongRenderer(filename)) if len(songlist) > before: break if len(songlist) == before: diff --git a/songbook_core/songs.py b/songbook_core/songs.py index 048e1ffa..5e280413 100644 --- a/songbook_core/songs.py +++ b/songbook_core/songs.py @@ -4,17 +4,12 @@ """Song management.""" from unidecode import unidecode -import glob import locale -import os.path import re -import logging from songbook_core.authors import processauthors from songbook_core.plastex import parsetex -LOGGER = logging.getLogger(__name__) - # pylint: disable=too-few-public-methods class Song(object): """Song management""" @@ -26,8 +21,10 @@ class Song(object): #: Dictionnaire des options pour le traitement des auteurs authwords = {"after": [], "ignore": [], "sep": []} - def __init__(self, path, languages, titles, args): - self.titles = titles + def __init__(self, filename): + # Data extraction from the song with plastex + data = parsetex(filename) + self.titles = data['titles'] self.normalized_titles = [ locale.strxfrm( unprefixed_title( @@ -36,11 +33,11 @@ class Song(object): ) ) for title - in titles + in self.titles ] - self.args = args - self.path = path - self.languages = languages + self.args = data['args'] + self.path = filename + self.languages = data['languages'] if "by" in self.args.keys(): self.normalized_authors = [ locale.strxfrm(author) @@ -87,23 +84,3 @@ def unprefixed_title(title, prefixes): return title -class SongbookContent(object): - """Manipulation et traitement de liste de chansons""" - - def __init__(self, datadirs): - self.songdirs = [os.path.join(d, 'songs') - for d in datadirs] - self.content = [] # Sorted list of the content - - def append_song(self, filename): - """Ajout d'une chanson à la liste - - Effets de bord : analyse syntaxique plus ou moins sommaire du fichier - pour en extraire et traiter certaines information (titre, langue, - album, etc.). - """ - LOGGER.debug('Parsing file "{}"…'.format(filename)) - # Data extraction from the song with plastex - data = parsetex(filename) - song = Song(filename, data['languages'], data['titles'], data['args']) - self.content.append(("song", song)) From e7519bce75896142cf3899564ef6b1b777be6869 Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 14 Jun 2014 15:35:37 +0200 Subject: [PATCH 08/27] [WIP] Song languages work --- songbook_core/content/song.py | 6 +++++- songbook_core/data/templates/songs.tex | 7 +++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/songbook_core/content/song.py b/songbook_core/content/song.py index fc371eaf..b2548933 100644 --- a/songbook_core/content/song.py +++ b/songbook_core/content/song.py @@ -34,6 +34,8 @@ class SongRenderer(Content, Song): return r'\input{{{}}}'.format(path) def parse(keyword, config, *arguments): + if 'languages' not in config: + config['languages'] = set() songlist = [] if not arguments: arguments = [ @@ -52,7 +54,9 @@ def parse(keyword, config, *arguments): for songdir in [os.path.join(d, 'songs') for d in config['datadir']]: for filename in glob.iglob(os.path.join(songdir, elem)): LOGGER.debug('Parsing file "{}"…'.format(filename)) - songlist.append(SongRenderer(filename)) + song = SongRenderer(filename) + songlist.append(song) + config["languages"].update(song.languages) if len(songlist) > before: break if len(songlist) == before: diff --git a/songbook_core/data/templates/songs.tex b/songbook_core/data/templates/songs.tex index 8c9b8066..f81db077 100644 --- a/songbook_core/data/templates/songs.tex +++ b/songbook_core/data/templates/songs.tex @@ -70,10 +70,9 @@ (* block songbookpreambule *) (( super() )) - %!TODO - %!(* for lang in content.languages() *) - %!\PassOptionsToPackage{((lang))}{babel} - %!(* endfor *) + (* for lang in languages *) + \PassOptionsToPackage{((lang))}{babel} + (* endfor *) \usepackage[((lang))]{babel} \lang{((lang))} From b7db9bcb6df6f8317729052da6a1ec883263829c Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 14 Jun 2014 15:54:21 +0200 Subject: [PATCH 09/27] [WIP] Managing titleprefixwords --- songbook_core/build.py | 18 ++++-------------- songbook_core/content/song.py | 2 +- songbook_core/songs.py | 6 ++---- 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/songbook_core/build.py b/songbook_core/build.py index cbef29a6..264d1e16 100755 --- a/songbook_core/build.py +++ b/songbook_core/build.py @@ -57,24 +57,15 @@ class Songbook(object): 'lang': 'english', 'sort': [u"by", u"album", u"@title"], 'content': None, + 'titleprefixwords': [], } self._parse_raw(raw_songbook) - @staticmethod - def _set_songs_default(config): - """Set the default values for the Song() class. - Argument: - - config : a dictionary containing the configuration - TODO Move this function elsewhere - """ - return - Song.sort = config['sort'] - if 'titleprefixwords' in config: - Song.prefixes = config['titleprefixwords'] - else: - Song.prefixes = [] + + + def __TODO(self): Song.authwords['after'] = [ re.compile(r"^.*%s\b(.*)" % after) for after @@ -142,7 +133,6 @@ class Songbook(object): context['content'] = self.contentlist context['filename'] = output.name[:-4] - self._set_songs_default(context) renderer.render_tex(output, context) diff --git a/songbook_core/content/song.py b/songbook_core/content/song.py index b2548933..27aca0e8 100644 --- a/songbook_core/content/song.py +++ b/songbook_core/content/song.py @@ -54,7 +54,7 @@ def parse(keyword, config, *arguments): for songdir in [os.path.join(d, 'songs') for d in config['datadir']]: for filename in glob.iglob(os.path.join(songdir, elem)): LOGGER.debug('Parsing file "{}"…'.format(filename)) - song = SongRenderer(filename) + song = SongRenderer(filename, config) songlist.append(song) config["languages"].update(song.languages) if len(songlist) > before: diff --git a/songbook_core/songs.py b/songbook_core/songs.py index 5e280413..27e59194 100644 --- a/songbook_core/songs.py +++ b/songbook_core/songs.py @@ -16,12 +16,10 @@ class Song(object): #: Ordre de tri sort = [] - #: Préfixes à ignorer pour le tri par titres - prefixes = [] #: Dictionnaire des options pour le traitement des auteurs authwords = {"after": [], "ignore": [], "sep": []} - def __init__(self, filename): + def __init__(self, filename, config): # Data extraction from the song with plastex data = parsetex(filename) self.titles = data['titles'] @@ -29,7 +27,7 @@ class Song(object): locale.strxfrm( unprefixed_title( unidecode(unicode(title, "utf-8")), - self.prefixes + config['titleprefixwords'] ) ) for title From 9f33b31f23456e010ed73cfea3c61f6c35ce36ad Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 14 Jun 2014 15:55:20 +0200 Subject: [PATCH 10/27] Deleting useless stuff (and preparing 'sort' content plugin) --- songbook_core/content/sorted.TODO | 23 +++++++++++++++++++++++ songbook_core/songs.py | 26 -------------------------- 2 files changed, 23 insertions(+), 26 deletions(-) create mode 100644 songbook_core/content/sorted.TODO diff --git a/songbook_core/content/sorted.TODO b/songbook_core/content/sorted.TODO new file mode 100644 index 00000000..2fd4679f --- /dev/null +++ b/songbook_core/content/sorted.TODO @@ -0,0 +1,23 @@ + def __cmp__(self, other): + if not isinstance(other, Song): + return NotImplemented + for key in self.sort: + if key == "@title": + self_key = self.normalized_titles + other_key = other.normalized_titles + elif key == "@path": + self_key = locale.strxfrm(self.path) + other_key = locale.strxfrm(other.path) + elif key == "by": + self_key = self.normalized_authors + other_key = other.normalized_authors + else: + self_key = locale.strxfrm(self.args.get(key, "")) + other_key = locale.strxfrm(other.args.get(key, "")) + + if self_key < other_key: + return -1 + elif self_key > other_key: + return 1 + return 0 + diff --git a/songbook_core/songs.py b/songbook_core/songs.py index 27e59194..e2222b85 100644 --- a/songbook_core/songs.py +++ b/songbook_core/songs.py @@ -14,8 +14,6 @@ from songbook_core.plastex import parsetex class Song(object): """Song management""" - #: Ordre de tri - sort = [] #: Dictionnaire des options pour le traitement des auteurs authwords = {"after": [], "ignore": [], "sep": []} @@ -48,30 +46,6 @@ class Song(object): def __repr__(self): return repr((self.titles, self.args, self.path)) - def __cmp__(self, other): - if not isinstance(other, Song): - return NotImplemented - for key in self.sort: - if key == "@title": - self_key = self.normalized_titles - other_key = other.normalized_titles - elif key == "@path": - self_key = locale.strxfrm(self.path) - other_key = locale.strxfrm(other.path) - elif key == "by": - self_key = self.normalized_authors - other_key = other.normalized_authors - else: - self_key = locale.strxfrm(self.args.get(key, "")) - other_key = locale.strxfrm(other.args.get(key, "")) - - if self_key < other_key: - return -1 - elif self_key > other_key: - return 1 - return 0 - - def unprefixed_title(title, prefixes): """Remove the first prefix of the list in the beginning of title (if any). """ From b695e303956dc0d6e2bdfc69aeb1f8561c411971 Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 14 Jun 2014 16:04:04 +0200 Subject: [PATCH 11/27] [WIP] Managing authwords --- songbook_core/build.py | 33 +++++++++++------------- songbook_core/data/templates/default.tex | 10 +++---- songbook_core/songs.py | 5 +--- 3 files changed, 21 insertions(+), 27 deletions(-) diff --git a/songbook_core/build.py b/songbook_core/build.py index 264d1e16..bc257aa7 100755 --- a/songbook_core/build.py +++ b/songbook_core/build.py @@ -62,23 +62,6 @@ class Songbook(object): self._parse_raw(raw_songbook) - - - - def __TODO(self): - Song.authwords['after'] = [ - re.compile(r"^.*%s\b(.*)" % after) - for after - in config['authwords']["after"] - ] - Song.authwords['ignore'] = config['authwords']['ignore'] - Song.authwords['sep'] = [ - re.compile(r"^(.*)%s (.*)$" % sep) - for sep in ([ - " %s" % sep for sep in config['authwords']["sep"] - ] + [',']) - ] - def _parse_raw(self, raw_songbook): """Parse raw_songbook. @@ -87,12 +70,26 @@ class Songbook(object): """ self.config.update(raw_songbook) self._set_datadir() + self._set_authwords() + - # TODO This should be moved elsewhere + def _set_authwords(self): # Ensure self.config['authwords'] contains all entries for (key, value) in DEFAULT_AUTHWORDS.items(): if key not in self.config['authwords']: self.config['authwords'][key] = value + # Convert strings to regular expressions + self.config["authwords"]['after'] = [ + re.compile(r"^.*%s\b(.*)" % after) + for after + in self.config['authwords']["after"] + ] + self.config["authwords"]['sep'] = [ + re.compile(r"^(.*)%s (.*)$" % sep) + for sep in ([ + " %s" % sep for sep in self.config['authwords']["sep"] + ] + [',']) + ] def _set_datadir(self): """Set the default values for datadir""" diff --git a/songbook_core/data/templates/default.tex b/songbook_core/data/templates/default.tex index 802d8806..6c756fad 100644 --- a/songbook_core/data/templates/default.tex +++ b/songbook_core/data/templates/default.tex @@ -98,11 +98,11 @@ (* endfor*) (* for key in titleprefixkeys *) (* for word in authwords.key *) - (* if key=="after" *) - \authbyword{((word))} - (* else *) - \auth((key))word{((word))} - (* endif *) + (* if key=="after" *) + \authbyword{((word))} + (* else *) + \auth((key))word{((word))} + (* endif *) (* endfor *) (* endfor*) diff --git a/songbook_core/songs.py b/songbook_core/songs.py index e2222b85..a3909d1c 100644 --- a/songbook_core/songs.py +++ b/songbook_core/songs.py @@ -14,9 +14,6 @@ from songbook_core.plastex import parsetex class Song(object): """Song management""" - #: Dictionnaire des options pour le traitement des auteurs - authwords = {"after": [], "ignore": [], "sep": []} - def __init__(self, filename, config): # Data extraction from the song with plastex data = parsetex(filename) @@ -38,7 +35,7 @@ class Song(object): self.normalized_authors = [ locale.strxfrm(author) for author - in processauthors(self.args["by"], **self.authwords) + in processauthors(self.args["by"], **config["authwords"]) ] else: self.normalized_authors = [] From 74745167e29a0e98e1376dc56712a1065fd469cb Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 14 Jun 2014 16:05:52 +0200 Subject: [PATCH 12/27] comment --- songbook_core/content/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/songbook_core/content/__init__.py b/songbook_core/content/__init__.py index 3b908868..cbc641d1 100644 --- a/songbook_core/content/__init__.py +++ b/songbook_core/content/__init__.py @@ -30,7 +30,7 @@ class Content: # Arguments - previous: the songbook.content.Content object of the previous item. - - context: TODO + - context: current jinja2.runtime.Context. # Return From 414716cdba6db42a7122ed01dea6e82cc8609538 Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 14 Jun 2014 16:47:35 +0200 Subject: [PATCH 13/27] Sorted out configuration defaults (between hard-coded defaults, templates, and configuration files) --- songbook_core/authors.py | 28 +++++++++++++++++ songbook_core/build.py | 67 +++++++++++++--------------------------- songbook_core/index.py | 26 ++-------------- 3 files changed, 52 insertions(+), 69 deletions(-) diff --git a/songbook_core/authors.py b/songbook_core/authors.py index cea20439..3f58b5af 100644 --- a/songbook_core/authors.py +++ b/songbook_core/authors.py @@ -3,6 +3,34 @@ """Authors string management.""" +import re + +DEFAULT_AUTHWORDS = { + "after": ["by"], + "ignore": ["unknown"], + "sep": ["and"], + } + +def compile_authwords(authwords): + # Convert strings to regular expressions + # Fill holes + for (key, value) in DEFAULT_AUTHWORDS.items(): + if key not in authwords: + authwords[key] = value + + # Compilation + authwords['after'] = [ + re.compile(r"^.*%s\b(.*)" % word) + for word in authwords['after'] + ] + authwords['sep'] = [ + re.compile(r"^(.*)%s (.*)$" % word) + for word in ([" %s" % word for word in authwords['sep']] + [',']) + ] + + return authwords + + def split_author_names(string): r"""Split author between first and last name. diff --git a/songbook_core/build.py b/songbook_core/build.py index bc257aa7..deeb5d55 100755 --- a/songbook_core/build.py +++ b/songbook_core/build.py @@ -11,6 +11,7 @@ import re from subprocess import Popen, PIPE, call from songbook_core import __DATADIR__ +from songbook_core import authors from songbook_core import content from songbook_core import errors from songbook_core.index import process_sxd @@ -19,11 +20,6 @@ from songbook_core.templates import TexRenderer LOGGER = logging.getLogger(__name__) EOL = "\n" -DEFAULT_AUTHWORDS = { - "after": ["by"], - "ignore": ["unknown"], - "sep": ["and"], - } DEFAULT_STEPS = ['tex', 'pdf', 'sbx', 'pdf', 'clean'] GENERATED_EXTENSIONS = [ "_auth.sbx", @@ -36,6 +32,12 @@ GENERATED_EXTENSIONS = [ "_title.sbx", "_title.sxd", ] +DEFAULT_CONFIG = { + 'template': "default.tex", + 'lang': 'english', + 'content': [], + 'titleprefixwords': [], + } @@ -50,46 +52,10 @@ class Songbook(object): def __init__(self, raw_songbook, basename): super(Songbook, self).__init__() + self.config = raw_songbook self.basename = basename - # Default values: will be updated while parsing raw_songbook - self.config = { - 'template': "default.tex", - 'lang': 'english', - 'sort': [u"by", u"album", u"@title"], - 'content': None, - 'titleprefixwords': [], - } - self._parse_raw(raw_songbook) - - - def _parse_raw(self, raw_songbook): - """Parse raw_songbook. - - The principle is: some special keys have their value processed; others - are stored verbatim in self.config. - """ - self.config.update(raw_songbook) + # Some special keys have their value processed. self._set_datadir() - self._set_authwords() - - - def _set_authwords(self): - # Ensure self.config['authwords'] contains all entries - for (key, value) in DEFAULT_AUTHWORDS.items(): - if key not in self.config['authwords']: - self.config['authwords'][key] = value - # Convert strings to regular expressions - self.config["authwords"]['after'] = [ - re.compile(r"^.*%s\b(.*)" % after) - for after - in self.config['authwords']["after"] - ] - self.config["authwords"]['sep'] = [ - re.compile(r"^(.*)%s (.*)$" % sep) - for sep in ([ - " %s" % sep for sep in self.config['authwords']["sep"] - ] + [',']) - ] def _set_datadir(self): """Set the default values for datadir""" @@ -110,21 +76,30 @@ class Songbook(object): self.config['datadir'] = abs_datadir + def build_config(self, from_templates): + config = DEFAULT_CONFIG + config.update(from_templates) + config.update(self.config) + + # Post-processing + config['authwords'] = authors.compile_authwords(config['authwords']) + + return config + def write_tex(self, output): """Build the '.tex' file corresponding to self. Arguments: - output: a file object, in which the file will be written. """ - self.contentlist = content.process_content(self.config['content'], self.config) renderer = TexRenderer( self.config['template'], self.config['datadir'], self.config['lang'], ) - context = renderer.get_variables() - context.update(self.config) + context = self.build_config(renderer.get_variables()) + self.contentlist = content.process_content(self.config['content'], context) context['render_content'] = content.render_content context['titleprefixkeys'] = ["after", "sep", "ignore"] context['content'] = self.contentlist diff --git a/songbook_core/index.py b/songbook_core/index.py index 78b64165..734e4ec3 100755 --- a/songbook_core/index.py +++ b/songbook_core/index.py @@ -13,7 +13,7 @@ import locale import re import codecs -from songbook_core.authors import processauthors +from songbook_core import authors from songbook_core.plastex import simpleparse EOL = u"\n" @@ -66,7 +66,6 @@ class Index(object): self.data = dict() self.keywords = dict() self.prefix_patterns = [] - self.authwords = {"after": [], "ignore": [], "sep": []} if indextype == "TITLE INDEX DATA FILE": self.indextype = "TITLE" elif indextype == "SCRIPTURE INDEX DATA FILE": @@ -100,26 +99,7 @@ class Index(object): )) if self.indextype == "AUTHOR": - for key in self.keywords: - if key in self.authwords: - self.authwords[key] = self.keywords[key] - for word in self.authwords.keys(): - if word in self.keywords: - if word == "after": - self.authwords[word] = [ - re.compile(r"^.*{after}\b(.*)".format(after=after)) - for after in self.keywords[word] - ] - elif word == "sep": - self.authwords[word] = [" {sep}".format(sep=sep) - for sep in self.authwords[word] - ] + [","] - self.authwords[word] = [ - re.compile(r"^(.*){sep} (.*)$".format(sep=sep)) - for sep in self.authwords[word] - ] - else: - self.authwords[word] = self.keywords[word] + self.authwords = authors.compile_authwords(self.keywords) def _raw_add(self, key, number, link): """Add a song to the list. @@ -157,7 +137,7 @@ class Index(object): if self.indextype == "AUTHOR": # Processing authors - for author in processauthors( + for author in authors.processauthors( key, **self.authwords): self._raw_add(author, number, link) From 7b4a42ec1d3966345759f7ecca3f32326e4bf946 Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 14 Jun 2014 16:55:37 +0200 Subject: [PATCH 14/27] Fix case where configuration file contain no content keyword at all --- songbook_core/build.py | 2 +- songbook_core/content/__init__.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/songbook_core/build.py b/songbook_core/build.py index deeb5d55..86c7433d 100755 --- a/songbook_core/build.py +++ b/songbook_core/build.py @@ -99,7 +99,7 @@ class Songbook(object): ) context = self.build_config(renderer.get_variables()) - self.contentlist = content.process_content(self.config['content'], context) + self.contentlist = content.process_content(self.config.get('content', []), context) context['render_content'] = content.render_content context['titleprefixkeys'] = ["after", "sep", "ignore"] context['content'] = self.contentlist diff --git a/songbook_core/content/__init__.py b/songbook_core/content/__init__.py index cbc641d1..b5aed178 100644 --- a/songbook_core/content/__init__.py +++ b/songbook_core/content/__init__.py @@ -101,6 +101,8 @@ def render_content(context, content): def process_content(content, config = None): contentlist = [] plugins = load_plugins() + if not content: + content = [["song"]] for elem in content: if isinstance(elem, basestring): elem = ["song", elem] From 0a4d7638a069c187072b1f93f8cc75275f5409a5 Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 14 Jun 2014 18:00:18 +0200 Subject: [PATCH 15/27] Added plugin 'sorted' --- songbook_core/content/__init__.py | 18 +++++++++++--- songbook_core/content/section.py | 8 +++---- songbook_core/content/song.py | 25 +++++++++++++++---- songbook_core/content/songsection.py | 6 ++--- songbook_core/content/sorted.TODO | 23 ------------------ songbook_core/content/sorted.py | 36 ++++++++++++++++++++++++++++ 6 files changed, 78 insertions(+), 38 deletions(-) delete mode 100644 songbook_core/content/sorted.TODO create mode 100644 songbook_core/content/sorted.py diff --git a/songbook_core/content/__init__.py b/songbook_core/content/__init__.py index b5aed178..2c3498a6 100644 --- a/songbook_core/content/__init__.py +++ b/songbook_core/content/__init__.py @@ -6,6 +6,7 @@ import importlib import jinja2 import logging import os +import re from songbook_core.errors import SongbookError @@ -101,6 +102,7 @@ def render_content(context, content): def process_content(content, config = None): contentlist = [] plugins = load_plugins() + keyword_re = re.compile(r'^ *(?P\w*) *(\((?P.*)\))? *$') if not content: content = [["song"]] for elem in content: @@ -108,7 +110,17 @@ def process_content(content, config = None): elem = ["song", elem] if len(content) == 0: content = ["song"] - if elem[0] not in plugins: - raise ContentError(elem[0], "Unknown content type.") - contentlist.extend(plugins[elem[0]](elem[0], config, *elem[1:])) + try: + match = keyword_re.match(elem[0]).groupdict() + except AttributeError: + raise ContentError(elem[0], "Cannot parse content type.") + (keyword, argument) = (match['keyword'], match['argument']) + if keyword not in plugins: + raise ContentError(keyword, "Unknown content type.") + contentlist.extend(plugins[keyword]( + keyword, + argument = argument, + contentlist = elem[1:], + config = config, + )) return contentlist diff --git a/songbook_core/content/section.py b/songbook_core/content/section.py index f2cf4e89..74f9e31e 100644 --- a/songbook_core/content/section.py +++ b/songbook_core/content/section.py @@ -26,12 +26,12 @@ class Section(Content): else: return r'\{}[{}]{{{}}}'.format(self.keyword, self.short, self.name) -def parse(keyword, config, *arguments): - if (keyword not in KEYWORDS) and (len(arguments) != 1): +def parse(keyword, argument, contentlist, config): + if (keyword not in KEYWORDS) and (len(contentlist) != 1): raise ContentError(keyword, "Starred section names must have exactly one argument.") - if (len(arguments) not in [1, 2]): + if (len(contentlist) not in [1, 2]): raise ContentError(keyword, "Section can have one or two arguments.") - return [Section(keyword, *arguments)] + return [Section(keyword, *contentlist)] CONTENT_PLUGINS = dict([(keyword, parse) for keyword in FULL_KEYWORDS]) diff --git a/songbook_core/content/song.py b/songbook_core/content/song.py index 27aca0e8..1aeaf15e 100644 --- a/songbook_core/content/song.py +++ b/songbook_core/content/song.py @@ -6,7 +6,7 @@ import jinja2 import logging import os -from songbook_core.content import Content +from songbook_core.content import Content, process_content, ContentError from songbook_core.files import recursive_find from songbook_core.songs import Song @@ -33,12 +33,12 @@ class SongRenderer(Content, Song): path = os.path.abspath(self.path) return r'\input{{{}}}'.format(path) -def parse(keyword, config, *arguments): +def parse(keyword, argument, contentlist, config): if 'languages' not in config: config['languages'] = set() songlist = [] - if not arguments: - arguments = [ + if not contentlist: + contentlist = [ os.path.relpath( filename, os.path.join(config['datadir'][0], 'songs'), @@ -49,7 +49,7 @@ def parse(keyword, config, *arguments): "*.sg" ) ] - for elem in arguments: + for elem in contentlist: before = len(songlist) for songdir in [os.path.join(d, 'songs') for d in config['datadir']]: for filename in glob.iglob(os.path.join(songdir, elem)): @@ -68,3 +68,18 @@ def parse(keyword, config, *arguments): CONTENT_PLUGINS = {'song': parse} + + +class OnlySongsError(ContentError): + def __init__(self, not_songs): + self.not_songs = not_songs + + def __str__(self): + return "Only songs are allowed, and the following items are not:" + str(not_songs) + +def process_songs(content, config = None): + contentlist = process_content(content, config) + not_songs = [item for item in contentlist if not isinstance(item, SongRenderer)] + if not_songs: + raise OnlySongsError(not_songs) + return contentlist diff --git a/songbook_core/content/songsection.py b/songbook_core/content/songsection.py index c27a3a31..1d1abd14 100644 --- a/songbook_core/content/songsection.py +++ b/songbook_core/content/songsection.py @@ -16,10 +16,10 @@ class SongSection(Content): def render(self, __context): return r'\{}{{{}}}'.format(self.keyword, self.name) -def parse(keyword, config, *arguments): - if (keyword not in KEYWORDS) and (len(arguments) != 1): +def parse(keyword, argument, contentlist, config): + if (keyword not in KEYWORDS) and (len(contentlist) != 1): raise ContentError(keyword, "Starred section names must have exactly one argument.") - return [SongSection(keyword, *arguments)] + return [SongSection(keyword, *contentlist)] CONTENT_PLUGINS = dict([(keyword, parse) for keyword in KEYWORDS]) diff --git a/songbook_core/content/sorted.TODO b/songbook_core/content/sorted.TODO deleted file mode 100644 index 2fd4679f..00000000 --- a/songbook_core/content/sorted.TODO +++ /dev/null @@ -1,23 +0,0 @@ - def __cmp__(self, other): - if not isinstance(other, Song): - return NotImplemented - for key in self.sort: - if key == "@title": - self_key = self.normalized_titles - other_key = other.normalized_titles - elif key == "@path": - self_key = locale.strxfrm(self.path) - other_key = locale.strxfrm(other.path) - elif key == "by": - self_key = self.normalized_authors - other_key = other.normalized_authors - else: - self_key = locale.strxfrm(self.args.get(key, "")) - other_key = locale.strxfrm(other.args.get(key, "")) - - if self_key < other_key: - return -1 - elif self_key > other_key: - return 1 - return 0 - diff --git a/songbook_core/content/sorted.py b/songbook_core/content/sorted.py new file mode 100644 index 00000000..6bd1c665 --- /dev/null +++ b/songbook_core/content/sorted.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import locale + +from songbook_core.content.song import OnlySongsError, process_songs + +DEFAULT_SORT = ['by', 'album', '@title'] + +def key_generator(sort): + def ordered_song_keys(song): + songkey = [] + for key in sort: + if key == "@title": + songkey.append(song.normalized_titles) + elif key == "@path": + songkey.append(locale.strxfrm(song.path)) + elif key == "by": + songkey.append(song.normalized_authors) + else: + songkey.append(locale.strxfrm(song.args.get(key, ""))) + return songkey + return ordered_song_keys + +def parse(keyword, config, argument, contentlist): + if argument: + sort = [key.strip() for key in argument.split(",")] + else: + sort = DEFAULT_SORT + try: + songlist = process_songs(contentlist, config) + except OnlySongsError as error: + raise ContentError(keyword, "Content list of this keyword can bo only songs (or content that result into songs), and the following are not:" + str(error.not_songs)) + return sorted(songlist, key=key_generator(sort)) + +CONTENT_PLUGINS = {'sorted': parse} From e87c6d4d3fcd71c57230e02dd92e50a46dec7fa1 Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 14 Jun 2014 20:09:39 +0200 Subject: [PATCH 16/27] Simplifying normalization of strings used to sort songs --- songbook_core/content/sorted.py | 18 ++++++++++++++---- songbook_core/index.py | 2 +- songbook_core/songs.py | 18 ++++++------------ 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/songbook_core/content/sorted.py b/songbook_core/content/sorted.py index 6bd1c665..d4ae45df 100644 --- a/songbook_core/content/sorted.py +++ b/songbook_core/content/sorted.py @@ -7,18 +7,28 @@ from songbook_core.content.song import OnlySongsError, process_songs DEFAULT_SORT = ['by', 'album', '@title'] +def normalize_string(string): + return locale.strxfrm(string.lower().strip()) + +def normalize_field(field): + if isinstance(field, basestring): + return normalize_string(field) + elif isinstance(field, list): + return [normalize_string(string) for string in field] + def key_generator(sort): def ordered_song_keys(song): songkey = [] for key in sort: if key == "@title": - songkey.append(song.normalized_titles) + field = song.unprefixed_titles elif key == "@path": - songkey.append(locale.strxfrm(song.path)) + field = song.path elif key == "by": - songkey.append(song.normalized_authors) + field = song.authors else: - songkey.append(locale.strxfrm(song.args.get(key, ""))) + field = song.args.get(key, "") + songkey.append(normalize_field(field)) return songkey return ordered_song_keys diff --git a/songbook_core/index.py b/songbook_core/index.py index 734e4ec3..67c1d410 100755 --- a/songbook_core/index.py +++ b/songbook_core/index.py @@ -30,7 +30,7 @@ def sortkey(value): don't forget to call locale.setlocale(locale.LC_ALL, '')). It also handles the sort with latex escape sequences. """ - return locale.strxfrm(unidecode(simpleparse(value).replace(' ', 'A'))) + return locale.strxfrm(unidecode(simpleparse(value).replace(' ', 'A')).lower()) def process_sxd(filename): diff --git a/songbook_core/songs.py b/songbook_core/songs.py index a3909d1c..f959466a 100644 --- a/songbook_core/songs.py +++ b/songbook_core/songs.py @@ -18,12 +18,10 @@ class Song(object): # Data extraction from the song with plastex data = parsetex(filename) self.titles = data['titles'] - self.normalized_titles = [ - locale.strxfrm( - unprefixed_title( - unidecode(unicode(title, "utf-8")), - config['titleprefixwords'] - ) + self.unprefixed_titles = [ + unprefixed_title( + unidecode(unicode(title, "utf-8")), + config['titleprefixwords'] ) for title in self.titles @@ -32,13 +30,9 @@ class Song(object): self.path = filename self.languages = data['languages'] if "by" in self.args.keys(): - self.normalized_authors = [ - locale.strxfrm(author) - for author - in processauthors(self.args["by"], **config["authwords"]) - ] + self.authors = processauthors(self.args["by"], **config["authwords"]) else: - self.normalized_authors = [] + self.authors = [] def __repr__(self): return repr((self.titles, self.args, self.path)) From fa8674d6a0c360a9df028a681c6c95d9d3854eed Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 14 Jun 2014 20:45:28 +0200 Subject: [PATCH 17/27] =?UTF-8?q?Introduction=20d'un=20param=C3=A8tre=20co?= =?UTF-8?q?nfig['songdir']=20;=20Peuplement=20par=20d=C3=A9faut=20de=20con?= =?UTF-8?q?tent=20avec=20le=20premier=20des=20r=C3=A9pertoires=20de=20song?= =?UTF-8?q?dir=20non=20vide.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- songbook_core/build.py | 1 + songbook_core/content/song.py | 16 ++++++---------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/songbook_core/build.py b/songbook_core/build.py index 86c7433d..5e26a720 100755 --- a/songbook_core/build.py +++ b/songbook_core/build.py @@ -75,6 +75,7 @@ class Songbook(object): abs_datadir.append(__DATADIR__) self.config['datadir'] = abs_datadir + self.config['songdir'] = [os.path.join(path, 'songs') for path in self.config['datadir']] def build_config(self, from_templates): config = DEFAULT_CONFIG diff --git a/songbook_core/content/song.py b/songbook_core/content/song.py index 1aeaf15e..38948e28 100644 --- a/songbook_core/content/song.py +++ b/songbook_core/content/song.py @@ -37,21 +37,17 @@ def parse(keyword, argument, contentlist, config): if 'languages' not in config: config['languages'] = set() songlist = [] - if not contentlist: + for songdir in config['songdir']: + if contentlist: + break contentlist = [ - os.path.relpath( - filename, - os.path.join(config['datadir'][0], 'songs'), - ) + os.path.relpath(filename, songdir) for filename - in recursive_find( - os.path.join(config['datadir'][0], 'songs'), - "*.sg" - ) + in recursive_find(songdir, "*.sg") ] for elem in contentlist: before = len(songlist) - for songdir in [os.path.join(d, 'songs') for d in config['datadir']]: + for songdir in config['songdir']: for filename in glob.iglob(os.path.join(songdir, elem)): LOGGER.debug('Parsing file "{}"…'.format(filename)) song = SongRenderer(filename, config) From 1e2dfc7d91e89da3d198e2e70bbed0a1966c53a9 Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 14 Jun 2014 20:46:14 +0200 Subject: [PATCH 18/27] =?UTF-8?q?Impl=C3=A9mentation=20du=20plugin=20cwd?= =?UTF-8?q?=20(#43)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- songbook_core/content/cwd.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 songbook_core/content/cwd.py diff --git a/songbook_core/content/cwd.py b/songbook_core/content/cwd.py new file mode 100644 index 00000000..6c76d8c2 --- /dev/null +++ b/songbook_core/content/cwd.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import os + +from songbook_core.content import process_content + +def parse(keyword, config, argument, contentlist): + config['songdir'] = ( + [os.path.relpath(argument)] + + [os.path.join(path, argument) for path in config['songdir']] + + config['songdir'] + ) + return process_content(contentlist, config) + +CONTENT_PLUGINS = {'cwd': parse} From 13322630ab69cc7146d1d334fd079c6529589c2a Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 14 Jun 2014 20:50:49 +0200 Subject: [PATCH 19/27] =?UTF-8?q?Introduction=20de=20variables=20de=20conf?= =?UTF-8?q?iguration=20priv=C3=A9es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les variables dont le nom commence par `_` sont privées : elles ne peuvent pas être assignées dans le fichier songbook ou dans un template, et sont destinées à un usage interne. --- songbook_core/build.py | 2 +- songbook_core/content/cwd.py | 6 +++--- songbook_core/content/song.py | 8 ++++---- songbook_core/data/templates/songs.tex | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/songbook_core/build.py b/songbook_core/build.py index 5e26a720..6a2a01e6 100755 --- a/songbook_core/build.py +++ b/songbook_core/build.py @@ -75,7 +75,7 @@ class Songbook(object): abs_datadir.append(__DATADIR__) self.config['datadir'] = abs_datadir - self.config['songdir'] = [os.path.join(path, 'songs') for path in self.config['datadir']] + self.config['_songdir'] = [os.path.join(path, 'songs') for path in self.config['datadir']] def build_config(self, from_templates): config = DEFAULT_CONFIG diff --git a/songbook_core/content/cwd.py b/songbook_core/content/cwd.py index 6c76d8c2..a9c5053e 100644 --- a/songbook_core/content/cwd.py +++ b/songbook_core/content/cwd.py @@ -6,10 +6,10 @@ import os from songbook_core.content import process_content def parse(keyword, config, argument, contentlist): - config['songdir'] = ( + config['_songdir'] = ( [os.path.relpath(argument)] + - [os.path.join(path, argument) for path in config['songdir']] + - config['songdir'] + [os.path.join(path, argument) for path in config['_songdir']] + + config['_songdir'] ) return process_content(contentlist, config) diff --git a/songbook_core/content/song.py b/songbook_core/content/song.py index 38948e28..fbaae8d7 100644 --- a/songbook_core/content/song.py +++ b/songbook_core/content/song.py @@ -35,9 +35,9 @@ class SongRenderer(Content, Song): def parse(keyword, argument, contentlist, config): if 'languages' not in config: - config['languages'] = set() + config['_languages'] = set() songlist = [] - for songdir in config['songdir']: + for songdir in config['_songdir']: if contentlist: break contentlist = [ @@ -47,12 +47,12 @@ def parse(keyword, argument, contentlist, config): ] for elem in contentlist: before = len(songlist) - for songdir in config['songdir']: + for songdir in config['_songdir']: for filename in glob.iglob(os.path.join(songdir, elem)): LOGGER.debug('Parsing file "{}"…'.format(filename)) song = SongRenderer(filename, config) songlist.append(song) - config["languages"].update(song.languages) + config["_languages"].update(song.languages) if len(songlist) > before: break if len(songlist) == before: diff --git a/songbook_core/data/templates/songs.tex b/songbook_core/data/templates/songs.tex index f81db077..7e51e0bb 100644 --- a/songbook_core/data/templates/songs.tex +++ b/songbook_core/data/templates/songs.tex @@ -70,7 +70,7 @@ (* block songbookpreambule *) (( super() )) - (* for lang in languages *) + (* for lang in _languages *) \PassOptionsToPackage{((lang))}{babel} (* endfor *) \usepackage[((lang))]{babel} From d06638681220c6df3c0ba7be17eb089af40f44f0 Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 14 Jun 2014 21:56:53 +0200 Subject: [PATCH 20/27] The code is pylint compliant. --- songbook | 2 +- songbook_core/authors.py | 7 +- songbook_core/build.py | 24 ++++- songbook_core/content/__init__.py | 133 +++++++++++++++++++++++---- songbook_core/content/cwd.py | 20 ++++ songbook_core/content/section.py | 37 ++++++-- songbook_core/content/song.py | 41 ++++++++- songbook_core/content/songsection.py | 29 +++++- songbook_core/content/sorted.py | 39 +++++++- songbook_core/index.py | 5 +- songbook_core/plastex.py | 1 + songbook_core/songs.py | 6 +- songbook_core/templates.py | 4 +- 13 files changed, 303 insertions(+), 45 deletions(-) diff --git a/songbook b/songbook index 40c3958a..c108b21f 100755 --- a/songbook +++ b/songbook @@ -32,7 +32,7 @@ class ParseStepsAction(argparse.Action): ( getattr(namespace, self.dest) + [value.strip() for value in values[0].split(',')] - ), + ), ) class VerboseAction(argparse.Action): diff --git a/songbook_core/authors.py b/songbook_core/authors.py index 3f58b5af..32afb09a 100644 --- a/songbook_core/authors.py +++ b/songbook_core/authors.py @@ -12,8 +12,11 @@ DEFAULT_AUTHWORDS = { } def compile_authwords(authwords): - # Convert strings to regular expressions - # Fill holes + """Convert strings of authwords to compiled regular expressions. + + 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 diff --git a/songbook_core/build.py b/songbook_core/build.py index 6a2a01e6..213cb8cb 100755 --- a/songbook_core/build.py +++ b/songbook_core/build.py @@ -7,7 +7,6 @@ import codecs import glob import logging import os.path -import re from subprocess import Popen, PIPE, call from songbook_core import __DATADIR__ @@ -15,7 +14,6 @@ from songbook_core import authors from songbook_core import content from songbook_core import errors from songbook_core.index import process_sxd -from songbook_core.songs import Song from songbook_core.templates import TexRenderer LOGGER = logging.getLogger(__name__) @@ -54,6 +52,7 @@ class Songbook(object): super(Songbook, self).__init__() self.config = raw_songbook self.basename = basename + self.contentlist = [] # Some special keys have their value processed. self._set_datadir() @@ -70,14 +69,26 @@ class Songbook(object): if os.path.exists(path) and os.path.isdir(path): abs_datadir.append(os.path.abspath(path)) else: - LOGGER.warning("Ignoring non-existent datadir '{}'.".format(path)) + LOGGER.warning( + "Ignoring non-existent datadir '{}'.".format(path) + ) abs_datadir.append(__DATADIR__) self.config['datadir'] = abs_datadir - self.config['_songdir'] = [os.path.join(path, 'songs') for path in self.config['datadir']] + self.config['_songdir'] = [ + os.path.join(path, 'songs') + for path in self.config['datadir'] + ] def build_config(self, from_templates): + """Build configuration dictionary + + This dictionary is assembled using (by order of least precedence): + - the hard-coded default; + - the values read from templates; + - the values read from .sb file. + """ config = DEFAULT_CONFIG config.update(from_templates) config.update(self.config) @@ -100,7 +111,10 @@ class Songbook(object): ) context = self.build_config(renderer.get_variables()) - self.contentlist = content.process_content(self.config.get('content', []), context) + self.contentlist = content.process_content( + self.config.get('content', []), + context, + ) context['render_content'] = content.render_content context['titleprefixkeys'] = ["after", "sep", "ignore"] context['content'] = self.contentlist diff --git a/songbook_core/content/__init__.py b/songbook_core/content/__init__.py index 2c3498a6..18757f73 100644 --- a/songbook_core/content/__init__.py +++ b/songbook_core/content/__init__.py @@ -1,6 +1,72 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +"""Content plugin management. + +Content that can be included in a songbook is controlled by plugins. From the +user (or .sb file) point of view, each piece of content is introduced by a +keyword. This keywold is associated with a plugin (a submodule of this very +module), which parses the content, and return a list of instances of the +Content class. + +# Plugin definition + +A plugin is a submodule of this module, which have a variable +CONTENT_PLUGINS, which is a dictionary where: + - keys are keywords, + - values are parsers (see below). + +When analysing the content field of the .sb file, when those keywords are +met, the corresponding parser is called. + +# Parsers + +A parser is a function which takes as arguments: + - keyword: the keyword triggering this function; + - argument: the argument of the keyword (see below); + - contentlist: the list of content, that is, the part of the list + following the keyword (see example below); + - config: the configuration object of the current songbook. Plugins can + change it. + +A parser returns a list of instances of the Content class, defined in +this module (or of subclasses of this class). + +Example: When the following piece of content is met + + ["sorted(author, @title)", "a_song.sg", "another_song.sg"] + +the parser associated to keyword 'sorted' get the arguments: + - keyword = "sorted" + - argument = "author, @title" + - contentlist = ["a_song.sg", "another_song.sg"] + - config = . + +# Keyword + +A keyword is either an identifier (alphanumeric characters, and underscore), +or such an identifier, with some text surrounded by parenthesis (like a +function definition); this text is called the argument to the keyword. +Examples: + - sorted + - sorted(author, @title) + - cwd(some/path) + +If the keyword has an argument, it can be anything, given that it is +surrounded by parenthesis. It is up to the plugin to parse this argument. For +intance, keyword "foo()(( bar()" is a perfectly valid keyword, and the parser +associated to "foo" will get as argument the string ")(( bar(". + +# Content class + +The content classes are subclasses of class Content defined in this module. +Content is a perfectly valid class, but instances of it will not generate +anything in the resulting .tex. + +More documentation in the docstring of Content. + +""" + import glob import importlib import jinja2 @@ -13,10 +79,15 @@ from songbook_core.errors import SongbookError LOGGER = logging.getLogger(__name__) EOL = '\n' -class Content: - """Content item of type 'example'.""" +#pylint: disable=no-self-use +class Content(object): + """Content item. Will render to something in the .tex file. + + The current jinja2.runtime.Context is passed to all function defined + here. + """ - def render(self, context): + def render(self, __context): """Render this content item. Returns a string, to be placed verbatim in the generated .tex file. @@ -25,31 +96,35 @@ class Content: # Block management - def begin_new_block(self, previous, context): + def begin_new_block(self, __previous, __context): """Return a boolean stating if a new block is to be created. # Arguments - - previous: the songbook.content.Content object of the previous item. - - context: current jinja2.runtime.Context. + - __previous: the songbook.content.Content object of the previous item. + - __context: see Content() documentation. # Return - True if the renderer has to close previous block, and begin a new one, - False otherwise. + - True if the renderer has to close previous block, and begin a new + one, + - False otherwise (the generated code for this item is part of the + current block). """ return True - def begin_block(self, context): + def begin_block(self, __context): """Return the string to begin a block.""" return "" - def end_block(self, context): + def end_block(self, __context): """Return the string to end a block.""" return "" class ContentError(SongbookError): + """Error in a content plugin.""" def __init__(self, keyword, message): + super(ContentError, self).__init__() self.keyword = keyword self.message = message @@ -67,17 +142,31 @@ def load_plugins(): for name in glob.glob(os.path.join(os.path.dirname(__file__), "*.py")): if name.endswith(".py") and os.path.basename(name) != "__init__.py": plugin = importlib.import_module( - 'songbook_core.content.{}'.format(os.path.basename(name[:-len('.py')])) - ) + 'songbook_core.content.{}'.format( + os.path.basename(name[:-len('.py')]) + ) + ) for (key, value) in plugin.CONTENT_PLUGINS.items(): if key in plugins: - LOGGER.warning("File %s: Keyword '%s' is already used. Ignored.", os.path.relpath(name), key) + LOGGER.warning( + "File %s: Keyword '%s' is already used. Ignored.", + os.path.relpath(name), + key, + ) continue plugins[key] = value return plugins @jinja2.contextfunction def render_content(context, content): + """Render the content of the songbook as a LaTeX code. + + Arguments: + - context: the jinja2.runtime.context of the current template + compilation. + - content: a list of Content() instances, as the one that was returned by + process_content(). + """ rendered = "" previous = None last = None @@ -99,7 +188,17 @@ def render_content(context, content): return rendered -def process_content(content, config = None): +def process_content(content, config=None): + """Process content, and return a list of Content() objects. + + Arguments are: + - content: the content field of the .sb file, which should be a list, and + describe what is to be included in the songbook; + - config: the configuration dictionary of the current songbook. + + Return: a list of Content objects, corresponding to the content to be + included in the .tex file. + """ contentlist = [] plugins = load_plugins() keyword_re = re.compile(r'^ *(?P\w*) *(\((?P.*)\))? *$') @@ -119,8 +218,8 @@ def process_content(content, config = None): raise ContentError(keyword, "Unknown content type.") contentlist.extend(plugins[keyword]( keyword, - argument = argument, - contentlist = elem[1:], - config = config, + argument=argument, + contentlist=elem[1:], + config=config, )) return contentlist diff --git a/songbook_core/content/cwd.py b/songbook_core/content/cwd.py index a9c5053e..e4a71992 100644 --- a/songbook_core/content/cwd.py +++ b/songbook_core/content/cwd.py @@ -1,11 +1,31 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +"""Change base directory before importing songs.""" + import os from songbook_core.content import process_content +#pylint: disable=unused-argument def parse(keyword, config, argument, contentlist): + """Return a list songs included in contentlist, whith a different base path. + + Arguments: + - keyword: unused; + - config: the current songbook configuration dictionary; + - argument: a directory; + - contentlist: songbook content, that is parsed by + songbook_core.content.process_content(). + + This function adds 'argument' to the directories where songs are searched + for, and then processes the content. + + The 'argument' is added: + - first as a relative path to the current directory; + - then as a relative path to every path already present in + config['songdir']. + """ config['_songdir'] = ( [os.path.relpath(argument)] + [os.path.join(path, argument) for path in config['_songdir']] + diff --git a/songbook_core/content/section.py b/songbook_core/content/section.py index 74f9e31e..7ce247bf 100644 --- a/songbook_core/content/section.py +++ b/songbook_core/content/section.py @@ -1,7 +1,9 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from songbook_core.content import Content +"""Allow LaTeX sections (starred or not) as content of a songbook.""" + +from songbook_core.content import Content, ContentError KEYWORDS = [ "part", @@ -12,26 +14,47 @@ KEYWORDS = [ "paragraph", "subparagraph", ] -FULL_KEYWORDS = KEYWORDS + [ "{}*".format(keyword) for keyword in KEYWORDS] +FULL_KEYWORDS = KEYWORDS + ["{}*".format(word) for word in KEYWORDS] class Section(Content): - def __init__(self, keyword, name, short = None): + """A LaTeX section.""" + + def __init__(self, keyword, name, short=None): self.keyword = keyword self.name = name self.short = short def render(self, __context): - if (self.short is None): + if self.short is None: return r'\{}{{{}}}'.format(self.keyword, self.name) else: return r'\{}[{}]{{{}}}'.format(self.keyword, self.short, self.name) +#pylint: disable=unused-argument def parse(keyword, argument, contentlist, config): + """Parse the contentlist. + + Arguments: + - keyword (one of "part", "chapter", "section", ... , "subparagraph", and + their starred versions "part*", "chapter*", ... , "subparagraph*"): the + section to use; + - argument: unused; + - contentlist: a list of one or two strings, which are the names (short + and long) of the section; + - config: configuration dictionary of the current songbook. + """ if (keyword not in KEYWORDS) and (len(contentlist) != 1): - raise ContentError(keyword, "Starred section names must have exactly one argument.") + raise ContentError( + keyword, + "Starred section names must have exactly one argument." + ) if (len(contentlist) not in [1, 2]): raise ContentError(keyword, "Section can have one or two arguments.") - return [Section(keyword, *contentlist)] + return [Section(keyword, *contentlist)] #pylint: disable=star-args -CONTENT_PLUGINS = dict([(keyword, parse) for keyword in FULL_KEYWORDS]) +CONTENT_PLUGINS = dict([ + (word, parse) + for word + in FULL_KEYWORDS + ]) diff --git a/songbook_core/content/song.py b/songbook_core/content/song.py index fbaae8d7..885aba6d 100644 --- a/songbook_core/content/song.py +++ b/songbook_core/content/song.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +"""Plugin to include songs to the songbook.""" + import glob import jinja2 import logging @@ -13,19 +15,25 @@ from songbook_core.songs import Song LOGGER = logging.getLogger(__name__) class SongRenderer(Content, Song): + """Render a song in the .tex file.""" + def begin_new_block(self, previous, __context): + """Return a boolean stating if a new block is to be created.""" return not isinstance(previous, SongRenderer) def begin_block(self, context): + """Return the string to begin a block.""" indexes = context.resolve("indexes") if isinstance(indexes, jinja2.runtime.Undefined): indexes = "" return r'\begin{songs}{%s}' % indexes def end_block(self, __context): + """Return the string to end a block.""" return r'\end{songs}' def render(self, context): + """Return the string that will render the song.""" outdir = os.path.dirname(context['filename']) if os.path.abspath(self.path).startswith(os.path.abspath(outdir)): path = os.path.relpath(self.path, outdir) @@ -33,7 +41,19 @@ class SongRenderer(Content, Song): path = os.path.abspath(self.path) return r'\input{{{}}}'.format(path) +#pylint: disable=unused-argument def parse(keyword, argument, contentlist, config): + """Parse data associated with keyword 'song'. + + Arguments: + - keyword: unused; + - argument: unused; + - contentlist: a list of strings, which are interpreted as regular + expressions (interpreted using the glob module), referring to songs. + - config: the current songbook configuration dictionary. + + Return a list of SongRenderer() instances. + """ if 'languages' not in config: config['_languages'] = set() songlist = [] @@ -67,15 +87,30 @@ CONTENT_PLUGINS = {'song': parse} class OnlySongsError(ContentError): + "A list that should contain only songs also contain other type of content." def __init__(self, not_songs): + super(OnlySongsError, self).__init__() self.not_songs = not_songs def __str__(self): - return "Only songs are allowed, and the following items are not:" + str(not_songs) + return ( + "Only songs are allowed, and the following items are not:" + + str(self.not_songs) + ) + +def process_songs(content, config=None): + """Process content that containt only songs. -def process_songs(content, config = None): + Call songbook_core.content.process_content(), checks if the returned list + contains only songs, and raise an exception if not. + """ contentlist = process_content(content, config) - not_songs = [item for item in contentlist if not isinstance(item, SongRenderer)] + not_songs = [ + item + for item + in contentlist + if not isinstance(item, SongRenderer) + ] if not_songs: raise OnlySongsError(not_songs) return contentlist diff --git a/songbook_core/content/songsection.py b/songbook_core/content/songsection.py index 1d1abd14..32ff40af 100644 --- a/songbook_core/content/songsection.py +++ b/songbook_core/content/songsection.py @@ -1,7 +1,9 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from songbook_core.content import Content +"""Allow 'songchapter' and 'songsection' as content of a songbook.""" + +from songbook_core.content import Content, ContentError KEYWORDS = [ "songchapter", @@ -9,17 +11,36 @@ KEYWORDS = [ ] class SongSection(Content): + """A songsection or songchapter.""" + def __init__(self, keyword, name): self.keyword = keyword self.name = name def render(self, __context): + """Render this section or chapter.""" return r'\{}{{{}}}'.format(self.keyword, self.name) +#pylint: disable=unused-argument def parse(keyword, argument, contentlist, config): + """Parse the contentlist. + + Arguments: + - keyword ("songsection" or "songchapter"): the section to use; + - argument: unused; + - contentlist: a list of one string, which is the name of the section; + - config: configuration dictionary of the current songbook. + """ if (keyword not in KEYWORDS) and (len(contentlist) != 1): - raise ContentError(keyword, "Starred section names must have exactly one argument.") - return [SongSection(keyword, *contentlist)] + raise ContentError( + keyword, + "Starred section names must have exactly one argument.", + ) + return [SongSection(keyword, contentlist[0])] -CONTENT_PLUGINS = dict([(keyword, parse) for keyword in KEYWORDS]) +CONTENT_PLUGINS = dict([ + (word, parse) + for word + in KEYWORDS + ]) diff --git a/songbook_core/content/sorted.py b/songbook_core/content/sorted.py index d4ae45df..030c6e12 100644 --- a/songbook_core/content/sorted.py +++ b/songbook_core/content/sorted.py @@ -1,23 +1,45 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +"""Sorted list of songs. + +This plugin provides keyword 'sorted', used to include a sorted list of songs +to a songbook. +""" + import locale +from songbook_core.content import ContentError from songbook_core.content.song import OnlySongsError, process_songs DEFAULT_SORT = ['by', 'album', '@title'] def normalize_string(string): + """Return a normalized string. + + Normalized means: + - no surrounding spaces; + - lower case; + - passed through locale.strxfrm(). + """ return locale.strxfrm(string.lower().strip()) def normalize_field(field): + """Return a normalized field, it being a string or a list of strings.""" if isinstance(field, basestring): return normalize_string(field) elif isinstance(field, list): return [normalize_string(string) for string in field] def key_generator(sort): + """Return a function that returns the list of values used to sort the song. + + Arguments: + - sort: the list of keys used to sort. + """ + def ordered_song_keys(song): + """Return the list of values used to sort the song.""" songkey = [] for key in sort: if key == "@title": @@ -32,7 +54,18 @@ def key_generator(sort): return songkey return ordered_song_keys +#pylint: disable=unused-argument def parse(keyword, config, argument, contentlist): + """Return a sorted list of songs contained in 'contentlist'. + + Arguments: + - keyword: the string 'sorted'; + - config: the current songbook configuration dictionary; + - argument: the list of the fields used to sort songs, as strings + separated by commas (e.g. "by, album, @title"); + - contentlist: the list of content to be sorted. If this content + contain something else than a song, an exception is raised. + """ if argument: sort = [key.strip() for key in argument.split(",")] else: @@ -40,7 +73,11 @@ def parse(keyword, config, argument, contentlist): try: songlist = process_songs(contentlist, config) except OnlySongsError as error: - raise ContentError(keyword, "Content list of this keyword can bo only songs (or content that result into songs), and the following are not:" + str(error.not_songs)) + raise ContentError(keyword, ( + "Content list of this keyword can bo only songs (or content " + "that result into songs), and the following are not:" + + str(error.not_songs) + )) return sorted(songlist, key=key_generator(sort)) CONTENT_PLUGINS = {'sorted': parse} diff --git a/songbook_core/index.py b/songbook_core/index.py index 67c1d410..50c5f946 100755 --- a/songbook_core/index.py +++ b/songbook_core/index.py @@ -30,7 +30,9 @@ def sortkey(value): don't forget to call locale.setlocale(locale.LC_ALL, '')). It also handles the sort with latex escape sequences. """ - return locale.strxfrm(unidecode(simpleparse(value).replace(' ', 'A')).lower()) + return locale.strxfrm( + unidecode(simpleparse(value).replace(' ', 'A')).lower() + ) def process_sxd(filename): @@ -65,6 +67,7 @@ class Index(object): def __init__(self, indextype): self.data = dict() self.keywords = dict() + self.authwords = dict() self.prefix_patterns = [] if indextype == "TITLE INDEX DATA FILE": self.indextype = "TITLE" diff --git a/songbook_core/plastex.py b/songbook_core/plastex.py index 7f5980ea..45bd0258 100644 --- a/songbook_core/plastex.py +++ b/songbook_core/plastex.py @@ -13,6 +13,7 @@ import sys def process_unbr_spaces(node): + #pylint: disable=line-too-long r"""Replace '~' and '\ ' in node by nodes that will be rendered as unbreakable space. diff --git a/songbook_core/songs.py b/songbook_core/songs.py index f959466a..51f56b52 100644 --- a/songbook_core/songs.py +++ b/songbook_core/songs.py @@ -4,7 +4,6 @@ """Song management.""" from unidecode import unidecode -import locale import re from songbook_core.authors import processauthors @@ -30,7 +29,10 @@ class Song(object): self.path = filename self.languages = data['languages'] if "by" in self.args.keys(): - self.authors = processauthors(self.args["by"], **config["authwords"]) + self.authors = processauthors( + self.args["by"], + **config["authwords"] + ) else: self.authors = [] diff --git a/songbook_core/templates.py b/songbook_core/templates.py index 05f2ba3d..18acb64c 100644 --- a/songbook_core/templates.py +++ b/songbook_core/templates.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- """Template for .tex generation settings and utilities""" -from jinja2 import Environment, FileSystemLoader, ChoiceLoader, PackageLoader, \ +from jinja2 import Environment, FileSystemLoader, ChoiceLoader, \ TemplateNotFound, nodes from jinja2.ext import Extension from jinja2.meta import find_referenced_templates as find_templates @@ -54,7 +54,7 @@ class VariablesExtension(Extension): end_tokens=['name:endvariables'], drop_needle=True, ) - return nodes.Const("") + return nodes.Const("") # pylint: disable=no-value-for-parameter def _escape_tex(value): From 099f21824cd19f6289fa99f6b193952563fe214c Mon Sep 17 00:00:00 2001 From: Louis Date: Sun, 15 Jun 2014 11:04:26 +0200 Subject: [PATCH 21/27] =?UTF-8?q?Correction=20de=20bug=20:=20r=C3=A9tablit?= =?UTF-8?q?=20le=20songdir=20original=20apr=C3=A8s=20traitement=20des=20fi?= =?UTF-8?q?chiers=20concern=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- songbook_core/content/cwd.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/songbook_core/content/cwd.py b/songbook_core/content/cwd.py index e4a71992..e5b40862 100644 --- a/songbook_core/content/cwd.py +++ b/songbook_core/content/cwd.py @@ -26,11 +26,14 @@ def parse(keyword, config, argument, contentlist): - then as a relative path to every path already present in config['songdir']. """ + old_songdir = config['_songdir'] config['_songdir'] = ( [os.path.relpath(argument)] + [os.path.join(path, argument) for path in config['_songdir']] + config['_songdir'] ) - return process_content(contentlist, config) + processed_content = process_content(contentlist, config) + config['_songdir'] = old_songdir + return processed_content CONTENT_PLUGINS = {'cwd': parse} From e6afeb94a1c700023f1034f02bc3a23255a7b758 Mon Sep 17 00:00:00 2001 From: Louis Date: Sun, 15 Jun 2014 11:04:53 +0200 Subject: [PATCH 22/27] Nouveau plugin : inclusion de fichiers LaTeX --- songbook_core/content/tex.py | 61 ++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 songbook_core/content/tex.py diff --git a/songbook_core/content/tex.py b/songbook_core/content/tex.py new file mode 100644 index 00000000..083053c4 --- /dev/null +++ b/songbook_core/content/tex.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""Include LaTeX raw code in the songbook.""" + +import logging +import os + +from songbook_core.content import Content, ContentError + +LOGGER = logging.getLogger(__name__) + +class LaTeX(Content): + """Inclusion of LaTeX code""" + + def __init__(self, filename): + self.filename = filename + + def render(self, context): + outdir = os.path.dirname(context['filename']) + if os.path.abspath(self.filename).startswith(os.path.abspath(outdir)): + filename = os.path.relpath(self.filename, outdir) + else: + filename = os.path.abspath(self.filename) + return r'\input{{{}}}'.format(filename) + +#pylint: disable=unused-argument +def parse(keyword, argument, contentlist, config): + """Parse the contentlist. + + Arguments: + - keyword: unused; + - argument: unused; + - contentlist: a list of name of tex files; + - config: configuration dictionary of the current songbook. + """ + if not contentlist: + LOGGER.warning( + "Useless 'tex' content: list of files to include is empty." + ) + filelist = [] + for filename in contentlist: + checked_file = None + for path in config['_songdir']: + if os.path.exists(os.path.join(path, filename)): + checked_file = os.path.relpath(os.path.join(path, filename)) + break + if not checked_file: + raise ContentError( + keyword, + "Cannot find file '{}' in '{}'.".format( + filename, + str(config['_songdir']), + ) + ) + filelist.append(LaTeX(checked_file)) + + return filelist + + +CONTENT_PLUGINS = {'tex': parse} From 5dc1cf415f88da003c6f89e4603073a95a511c6c Mon Sep 17 00:00:00 2001 From: Louis Date: Mon, 16 Jun 2014 06:49:11 +0200 Subject: [PATCH 23/27] Solving error in management of default values of configuration --- songbook_core/build.py | 46 +++++++++++++++++------------------------- 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/songbook_core/build.py b/songbook_core/build.py index 213cb8cb..2c8e5fa3 100755 --- a/songbook_core/build.py +++ b/songbook_core/build.py @@ -81,46 +81,38 @@ class Songbook(object): for path in self.config['datadir'] ] - def build_config(self, from_templates): - """Build configuration dictionary - - This dictionary is assembled using (by order of least precedence): - - the hard-coded default; - - the values read from templates; - - the values read from .sb file. - """ - config = DEFAULT_CONFIG - config.update(from_templates) - config.update(self.config) - - # Post-processing - config['authwords'] = authors.compile_authwords(config['authwords']) - - return config - def write_tex(self, output): """Build the '.tex' file corresponding to self. Arguments: - output: a file object, in which the file will be written. """ + # Updating configuration + config = DEFAULT_CONFIG + config.update(self.config) renderer = TexRenderer( - self.config['template'], - self.config['datadir'], - self.config['lang'], + config['template'], + config['datadir'], + config['lang'], ) + config.update(self.config) + config.update(renderer.get_variables()) + + config['authwords'] = authors.compile_authwords(config['authwords']) + + self.config = config + # Configuration set - context = self.build_config(renderer.get_variables()) self.contentlist = content.process_content( self.config.get('content', []), - context, + self.config, ) - context['render_content'] = content.render_content - context['titleprefixkeys'] = ["after", "sep", "ignore"] - context['content'] = self.contentlist - context['filename'] = output.name[:-4] + self.config['render_content'] = content.render_content + self.config['titleprefixkeys'] = ["after", "sep", "ignore"] + self.config['content'] = self.contentlist + self.config['filename'] = output.name[:-4] - renderer.render_tex(output, context) + renderer.render_tex(output, self.config) class SongbookBuilder(object): From f727f13a2be497359d042f216569121b8803714f Mon Sep 17 00:00:00 2001 From: Louis Date: Mon, 16 Jun 2014 19:33:58 +0200 Subject: [PATCH 24/27] Remplacement d'une erreur par un warning --- songbook_core/content/tex.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/songbook_core/content/tex.py b/songbook_core/content/tex.py index 083053c4..a5b5f2da 100644 --- a/songbook_core/content/tex.py +++ b/songbook_core/content/tex.py @@ -6,7 +6,7 @@ import logging import os -from songbook_core.content import Content, ContentError +from songbook_core.content import Content LOGGER = logging.getLogger(__name__) @@ -46,13 +46,11 @@ def parse(keyword, argument, contentlist, config): checked_file = os.path.relpath(os.path.join(path, filename)) break if not checked_file: - raise ContentError( - keyword, - "Cannot find file '{}' in '{}'.".format( - filename, - str(config['_songdir']), - ) + LOGGER.warning( + ("Cannot find file '{}' in '{}'. Compilation may fail " + "later.").format( filename, str(config['_songdir'])) ) + continue filelist.append(LaTeX(checked_file)) return filelist From 93a421fadbe56ef8d654d3009913d47bc4ae9a5c Mon Sep 17 00:00:00 2001 From: Louis Date: Tue, 17 Jun 2014 09:49:15 +0200 Subject: [PATCH 25/27] typo --- songbook_core/content/sorted.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/songbook_core/content/sorted.py b/songbook_core/content/sorted.py index 030c6e12..e7283f09 100644 --- a/songbook_core/content/sorted.py +++ b/songbook_core/content/sorted.py @@ -74,7 +74,7 @@ def parse(keyword, config, argument, contentlist): songlist = process_songs(contentlist, config) except OnlySongsError as error: raise ContentError(keyword, ( - "Content list of this keyword can bo only songs (or content " + "Content list of this keyword can be only songs (or content " "that result into songs), and the following are not:" + str(error.not_songs) )) From 2e12374e27b543b23309b2f8a0c4b848bc3bc5fd Mon Sep 17 00:00:00 2001 From: Louis Date: Tue, 17 Jun 2014 09:59:47 +0200 Subject: [PATCH 26/27] Ajout d'un message en cas de clef de tri incorrecte --- songbook_core/content/sorted.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/songbook_core/content/sorted.py b/songbook_core/content/sorted.py index e7283f09..4873cb3c 100644 --- a/songbook_core/content/sorted.py +++ b/songbook_core/content/sorted.py @@ -8,10 +8,14 @@ to a songbook. """ import locale +import logging +import os from songbook_core.content import ContentError from songbook_core.content.song import OnlySongsError, process_songs +LOGGER = logging.getLogger(__name__) + DEFAULT_SORT = ['by', 'album', '@title'] def normalize_string(string): @@ -49,7 +53,16 @@ def key_generator(sort): elif key == "by": field = song.authors else: - field = song.args.get(key, "") + try: + field = song.args[key] + except KeyError: + LOGGER.debug( + "Ignoring non-existent key '{}' for song {}.".format( + key, + os.path.relpath(song.path), + ) + ) + field = "" songkey.append(normalize_field(field)) return songkey return ordered_song_keys From 710ffe5467f1e6f20f099f07ec3d2716ca7d8352 Mon Sep 17 00:00:00 2001 From: Louis Date: Tue, 17 Jun 2014 10:12:45 +0200 Subject: [PATCH 27/27] Unification du traitement des chemins relatifs --- songbook_core/content/__init__.py | 3 ++- songbook_core/content/cwd.py | 2 +- songbook_core/content/song.py | 16 +++++++--------- songbook_core/content/sorted.py | 6 +++--- songbook_core/content/tex.py | 13 ++++++------- songbook_core/files.py | 9 +++++++++ 6 files changed, 28 insertions(+), 21 deletions(-) diff --git a/songbook_core/content/__init__.py b/songbook_core/content/__init__.py index 18757f73..6096c6dc 100644 --- a/songbook_core/content/__init__.py +++ b/songbook_core/content/__init__.py @@ -74,6 +74,7 @@ import logging import os import re +from songbook_core import files from songbook_core.errors import SongbookError LOGGER = logging.getLogger(__name__) @@ -150,7 +151,7 @@ def load_plugins(): if key in plugins: LOGGER.warning( "File %s: Keyword '%s' is already used. Ignored.", - os.path.relpath(name), + files.relpath(name), key, ) continue diff --git a/songbook_core/content/cwd.py b/songbook_core/content/cwd.py index e5b40862..c1d3cc36 100644 --- a/songbook_core/content/cwd.py +++ b/songbook_core/content/cwd.py @@ -28,7 +28,7 @@ def parse(keyword, config, argument, contentlist): """ old_songdir = config['_songdir'] config['_songdir'] = ( - [os.path.relpath(argument)] + + [argument] + [os.path.join(path, argument) for path in config['_songdir']] + config['_songdir'] ) diff --git a/songbook_core/content/song.py b/songbook_core/content/song.py index 885aba6d..432efdad 100644 --- a/songbook_core/content/song.py +++ b/songbook_core/content/song.py @@ -9,7 +9,7 @@ import logging import os from songbook_core.content import Content, process_content, ContentError -from songbook_core.files import recursive_find +from songbook_core import files from songbook_core.songs import Song LOGGER = logging.getLogger(__name__) @@ -34,12 +34,10 @@ class SongRenderer(Content, Song): def render(self, context): """Return the string that will render the song.""" - outdir = os.path.dirname(context['filename']) - if os.path.abspath(self.path).startswith(os.path.abspath(outdir)): - path = os.path.relpath(self.path, outdir) - else: - path = os.path.abspath(self.path) - return r'\input{{{}}}'.format(path) + return r'\input{{{}}}'.format(files.relpath( + self.path, + os.path.dirname(context['filename']) + )) #pylint: disable=unused-argument def parse(keyword, argument, contentlist, config): @@ -61,9 +59,9 @@ def parse(keyword, argument, contentlist, config): if contentlist: break contentlist = [ - os.path.relpath(filename, songdir) + files.relpath(filename, songdir) for filename - in recursive_find(songdir, "*.sg") + in files.recursive_find(songdir, "*.sg") ] for elem in contentlist: before = len(songlist) diff --git a/songbook_core/content/sorted.py b/songbook_core/content/sorted.py index 4873cb3c..61a7499f 100644 --- a/songbook_core/content/sorted.py +++ b/songbook_core/content/sorted.py @@ -9,8 +9,8 @@ to a songbook. import locale import logging -import os +from songbook_core import files from songbook_core.content import ContentError from songbook_core.content.song import OnlySongsError, process_songs @@ -57,9 +57,9 @@ def key_generator(sort): field = song.args[key] except KeyError: LOGGER.debug( - "Ignoring non-existent key '{}' for song {}.".format( + "Ignoring unknown key '{}' for song {}.".format( key, - os.path.relpath(song.path), + files.relpath(song.path), ) ) field = "" diff --git a/songbook_core/content/tex.py b/songbook_core/content/tex.py index a5b5f2da..a1c6ec41 100644 --- a/songbook_core/content/tex.py +++ b/songbook_core/content/tex.py @@ -6,6 +6,7 @@ import logging import os +from songbook_core import files from songbook_core.content import Content LOGGER = logging.getLogger(__name__) @@ -17,12 +18,10 @@ class LaTeX(Content): self.filename = filename def render(self, context): - outdir = os.path.dirname(context['filename']) - if os.path.abspath(self.filename).startswith(os.path.abspath(outdir)): - filename = os.path.relpath(self.filename, outdir) - else: - filename = os.path.abspath(self.filename) - return r'\input{{{}}}'.format(filename) + return r'\input{{{}}}'.format(files.relpath( + self.filename, + os.path.dirname(context['filename']), + )) #pylint: disable=unused-argument def parse(keyword, argument, contentlist, config): @@ -48,7 +47,7 @@ def parse(keyword, argument, contentlist, config): if not checked_file: LOGGER.warning( ("Cannot find file '{}' in '{}'. Compilation may fail " - "later.").format( filename, str(config['_songdir'])) + "later.").format(filename, str(config['_songdir'])) ) continue filelist.append(LaTeX(checked_file)) diff --git a/songbook_core/files.py b/songbook_core/files.py index 65acc153..31cc4e2e 100644 --- a/songbook_core/files.py +++ b/songbook_core/files.py @@ -17,3 +17,12 @@ def recursive_find(root_directory, pattern): for filename in fnmatch.filter(filenames, pattern): matches.append(os.path.join(root, filename)) return matches + +def relpath(path, start=None): + """Return relative filepath to path if a subpath of start.""" + if start is None: + start = os.curdir + if os.path.abspath(path).startswith(os.path.abspath(start)): + return os.path.relpath(path, start) + else: + return os.path.abspath(path)