Browse Source

[WIP] Content errors are now gathered

* Rename `patacrep.content.Content` to `patacrep.content.ContentItem`
* Create pseudo-list `patacrep.content.ContentList`, which has an
  `errors` attribute.
* Songs that cannot be parsed at all no longer produce an empty song.

This is highly work in progress: most of the `patacrep.content.*`
packages are broken.
pull/176/head
Louis 9 years ago
parent
commit
d184f037fc
  1. 68
      patacrep/content/__init__.py
  2. 6
      patacrep/content/section.py
  3. 12
      patacrep/content/song.py
  4. 6
      patacrep/content/songsection.py
  5. 6
      patacrep/content/tex.py
  6. 11
      patacrep/songs/chordpro/syntax.py

68
patacrep/content/__init__.py

@ -3,8 +3,8 @@
Content that can be included in a songbook is controlled by plugins. From the 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 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 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 module), which parses the content, and return a ContentList object, which is
Content class. little more than a list of instances of the ContentItem class.
# Plugin definition # Plugin definition
@ -27,8 +27,8 @@ A parser is a function which takes as arguments:
- config: the configuration object of the current songbook. Plugins can - config: the configuration object of the current songbook. Plugins can
change it. change it.
A parser returns a list of instances of the Content class, defined in A parser returns a ContentList object (a list of instances of the ContentItem
this module (or of subclasses of this class). class), defined in this module (or of subclasses of this class).
Example: When the following piece of content is met Example: When the following piece of content is met
@ -55,13 +55,13 @@ 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 intance, keyword "foo()(( bar()" is a perfectly valid keyword, and the parser
associated to "foo" will get as argument the string ")(( bar(". associated to "foo" will get as argument the string ")(( bar(".
# Content class # ContentItem class
The content classes are subclasses of class Content defined in this module. The content classes are subclasses of class ContentItem defined in this module.
Content is a perfectly valid class, but instances of it will not generate ContentItem is a perfectly valid class, but instances of it will not generate
anything in the resulting .tex. anything in the resulting .tex.
More documentation in the docstring of Content. More documentation in the docstring of ContentItem.
""" """
@ -79,7 +79,7 @@ LOGGER = logging.getLogger(__name__)
EOL = '\n' EOL = '\n'
#pylint: disable=no-self-use #pylint: disable=no-self-use
class Content: class ContentItem:
"""Content item. Will render to something in the .tex file. """Content item. Will render to something in the .tex file.
The current jinja2.runtime.Context is passed to all function defined The current jinja2.runtime.Context is passed to all function defined
@ -100,8 +100,8 @@ class Content:
# Arguments # Arguments
- __previous: the songbook.content.Content object of the previous item. - __previous: the songbook.content.ContentItem object of the previous item.
- __context: see Content() documentation. - __context: see ContentItem() documentation.
# Return # Return
@ -122,13 +122,37 @@ class Content:
class ContentError(SongbookError): class ContentError(SongbookError):
"""Error in a content plugin.""" """Error in a content plugin."""
def __init__(self, keyword, message): def __init__(self, keyword=None, message=None):
super(ContentError, self).__init__() super(ContentError, self).__init__()
self.keyword = keyword self.keyword = keyword
self.message = message self.message = message
def __str__(self): def __str__(self):
return "Content: {}: {}".format(self.keyword, self.message) text = "Content"
if self.keyword is not None:
text += ": " + self.keyword
if self.message is not None:
text += ": " + self.message
return text
class ContentList:
"""List of content items"""
def __init__(self, *args, **kwargs):
self._content = list(*args, **kwargs)
self.errors = []
def __iter__(self):
yield from self._content
def extend(self, iterator):
return self._content.extend(iterator)
def append(self, item):
return self._content.append(item)
def __len__(self):
return len(self._content)
@jinja2.contextfunction @jinja2.contextfunction
def render(context, content): def render(context, content):
@ -137,14 +161,14 @@ def render(context, content):
Arguments: Arguments:
- context: the jinja2.runtime.context of the current template - context: the jinja2.runtime.context of the current template
compilation. compilation.
- content: a list of Content() instances, as the one that was returned by - content: a list of ContentItem() instances, as the one that was returned by
process_content(). process_content().
""" """
rendered = "" rendered = ""
previous = None previous = None
last = None last = None
for elem in content: for elem in content:
if not isinstance(elem, Content): if not isinstance(elem, ContentItem):
LOGGER.warning("Ignoring bad content item '{}'.".format(elem)) LOGGER.warning("Ignoring bad content item '{}'.".format(elem))
continue continue
@ -156,23 +180,23 @@ def render(context, content):
rendered += elem.render(context) + EOL rendered += elem.render(context) + EOL
previous = elem previous = elem
if isinstance(last, Content): if isinstance(last, ContentItem):
rendered += last.end_block(context) + EOL rendered += last.end_block(context) + EOL
return rendered return rendered
def process_content(content, config=None): def process_content(content, config=None):
"""Process content, and return a list of Content() objects. """Process content, and return a list of ContentItem() objects.
Arguments are: Arguments are:
- content: the content field of the .sb file, which should be a list, and - content: the content field of the .sb file, which should be a list, and
describe what is to be included in the songbook; describe what is to be included in the songbook;
- config: the configuration dictionary of the current songbook. - config: the configuration dictionary of the current songbook.
Return: a list of Content objects, corresponding to the content to be Return: a list of ContentItem objects, corresponding to the content to be
included in the .tex file. included in the .tex file.
""" """
contentlist = [] contentlist = ContentList()
plugins = config.get('_content_plugins', {}) plugins = config.get('_content_plugins', {})
keyword_re = re.compile(r'^ *(?P<keyword>\w*) *(\((?P<argument>.*)\))? *$') keyword_re = re.compile(r'^ *(?P<keyword>\w*) *(\((?P<argument>.*)\))? *$')
if not content: if not content:
@ -185,10 +209,12 @@ def process_content(content, config=None):
try: try:
match = keyword_re.match(elem[0]).groupdict() match = keyword_re.match(elem[0]).groupdict()
except AttributeError: except AttributeError:
raise ContentError(elem[0], "Cannot parse content type.") contentlist.errors.append(ContentError(elem[0], "Cannot parse content type."))
continue
(keyword, argument) = (match['keyword'], match['argument']) (keyword, argument) = (match['keyword'], match['argument'])
if keyword not in plugins: if keyword not in plugins:
raise ContentError(keyword, "Unknown content type.") contentlist.errors.append(ContentError(keyword, "Unknown content type."))
continue
contentlist.extend(plugins[keyword]( contentlist.extend(plugins[keyword](
keyword, keyword,
argument=argument, argument=argument,

6
patacrep/content/section.py

@ -1,6 +1,6 @@
"""Allow LaTeX sections (starred or not) as content of a songbook.""" """Allow LaTeX sections (starred or not) as content of a songbook."""
from patacrep.content import Content, ContentError from patacrep.content import ContentItem, ContentError
KEYWORDS = [ KEYWORDS = [
"part", "part",
@ -13,7 +13,7 @@ KEYWORDS = [
] ]
FULL_KEYWORDS = KEYWORDS + ["{}*".format(word) for word in KEYWORDS] FULL_KEYWORDS = KEYWORDS + ["{}*".format(word) for word in KEYWORDS]
class Section(Content): class Section(ContentItem):
"""A LaTeX section.""" """A LaTeX section."""
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
@ -48,7 +48,7 @@ def parse(keyword, argument, contentlist, config):
) )
if (len(contentlist) not in [1, 2]): if (len(contentlist) not in [1, 2]):
raise ContentError(keyword, "Section can have one or two arguments.") raise ContentError(keyword, "Section can have one or two arguments.")
return [Section(keyword, *contentlist)] return ContentList(Section(keyword, *contentlist))
CONTENT_PLUGINS = dict([ CONTENT_PLUGINS = dict([

12
patacrep/content/song.py

@ -6,12 +6,13 @@ import logging
import os import os
import textwrap import textwrap
from patacrep.content import process_content, ContentError, Content from patacrep.content import process_content
from patacrep.content import ContentError, ContentItem, ContentList
from patacrep import files, errors from patacrep import files, errors
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
class SongRenderer(Content): class SongRenderer(ContentItem):
"""Render a song in as a tex code.""" """Render a song in as a tex code."""
def __init__(self, song): def __init__(self, song):
@ -71,7 +72,7 @@ def parse(keyword, argument, contentlist, config):
plugins = config['_song_plugins'] plugins = config['_song_plugins']
if '_langs' not in config: if '_langs' not in config:
config['_langs'] = set() config['_langs'] = set()
songlist = [] songlist = ContentList()
for songdir in config['_songdir']: for songdir in config['_songdir']:
if contentlist: if contentlist:
break break
@ -95,11 +96,16 @@ def parse(keyword, argument, contentlist, config):
", ".join(["'.{}'".format(key) for key in plugins.keys()]), ", ".join(["'.{}'".format(key) for key in plugins.keys()]),
)) ))
continue continue
try:
renderer = SongRenderer(plugins[extension]( renderer = SongRenderer(plugins[extension](
filename, filename,
config, config,
datadir=songdir.datadir, datadir=songdir.datadir,
)) ))
except ContentError as error:
# TODO
songlist.errors.append(error)
continue
songlist.append(renderer) songlist.append(renderer)
config["_langs"].add(renderer.song.lang) config["_langs"].add(renderer.song.lang)
if len(songlist) > before: if len(songlist) > before:

6
patacrep/content/songsection.py

@ -1,13 +1,13 @@
"""Allow 'songchapter' and 'songsection' as content of a songbook.""" """Allow 'songchapter' and 'songsection' as content of a songbook."""
from patacrep.content import Content, ContentError from patacrep.content import ContentItem, ContentError
KEYWORDS = [ KEYWORDS = [
"songchapter", "songchapter",
"songsection", "songsection",
] ]
class SongSection(Content): class SongSection(ContentItem):
"""A songsection or songchapter.""" """A songsection or songchapter."""
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
@ -34,7 +34,7 @@ def parse(keyword, argument, contentlist, config):
keyword, keyword,
"Starred section names must have exactly one argument.", "Starred section names must have exactly one argument.",
) )
return [SongSection(keyword, contentlist[0])] return ContentList(SongSection(keyword, contentlist[0]))
CONTENT_PLUGINS = dict([ CONTENT_PLUGINS = dict([

6
patacrep/content/tex.py

@ -5,11 +5,11 @@ import logging
import os import os
from patacrep import files, errors from patacrep import files, errors
from patacrep.content import Content from patacrep.content import ContentItem, ContentList
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
class LaTeX(Content): class LaTeX(ContentItem):
"""Inclusion of LaTeX code""" """Inclusion of LaTeX code"""
def __init__(self, filename): def __init__(self, filename):
@ -35,7 +35,7 @@ def parse(keyword, argument, contentlist, config):
LOGGER.warning( LOGGER.warning(
"Useless 'tex' content: list of files to include is empty." "Useless 'tex' content: list of files to include is empty."
) )
filelist = [] filelist = ContentList
basefolders = itertools.chain( basefolders = itertools.chain(
(path.fullpath for path in config['_songdir']), (path.fullpath for path in config['_songdir']),
files.iter_datadirs(config['datadir']), files.iter_datadirs(config['datadir']),

11
patacrep/songs/chordpro/syntax.py

@ -5,6 +5,7 @@ import logging
import ply.yacc as yacc import ply.yacc as yacc
import re import re
from patacrep.content import ContentError
from patacrep.songs import errors from patacrep.songs import errors
from patacrep.songs.chordpro import ast from patacrep.songs.chordpro import ast
from patacrep.songs.chordpro.lexer import tokens, ChordProLexer from patacrep.songs.chordpro.lexer import tokens, ChordProLexer
@ -329,13 +330,5 @@ def parse_song(content, filename=None):
lexer=ChordProLexer(filename=filename).lexer, lexer=ChordProLexer(filename=filename).lexer,
) )
if parsed_content is None: if parsed_content is None:
return ast.Song( raise ContentError(message='Fatal error during song parsing.')
filename,
[],
errors=[functools.partial(
errors.SongSyntaxError,
line=parser._errors[-1].keywords['line'],
message='Fatal error during song parsing.',
)]
)
return parsed_content return parsed_content

Loading…
Cancel
Save