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
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.
module), which parses the content, and return a ContentList object, which is
little more than a list of instances of the ContentItem class.
# 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
change it.
A parser returns a list of instances of the Content class, defined in
this module (or of subclasses of this class).
A parser returns a ContentList object (a list of instances of the ContentItem
class), defined in this module (or of subclasses of this class).
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
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.
Content is a perfectly valid class, but instances of it will not generate
The content classes are subclasses of class ContentItem defined in this module.
ContentItem is a perfectly valid class, but instances of it will not generate
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'
#pylint: disable=no-self-use
class Content:
class ContentItem:
"""Content item. Will render to something in the .tex file.
The current jinja2.runtime.Context is passed to all function defined
@ -100,8 +100,8 @@ class Content:
# Arguments
- __previous: the songbook.content.Content object of the previous item.
- __context: see Content() documentation.
- __previous: the songbook.content.ContentItem object of the previous item.
- __context: see ContentItem() documentation.
# Return
@ -122,13 +122,37 @@ class Content:
class ContentError(SongbookError):
"""Error in a content plugin."""
def __init__(self, keyword, message):
def __init__(self, keyword=None, message=None):
super(ContentError, self).__init__()
self.keyword = keyword
self.message = message
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
def render(context, content):
@ -137,14 +161,14 @@ def render(context, content):
Arguments:
- context: the jinja2.runtime.context of the current template
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().
"""
rendered = ""
previous = None
last = None
for elem in content:
if not isinstance(elem, Content):
if not isinstance(elem, ContentItem):
LOGGER.warning("Ignoring bad content item '{}'.".format(elem))
continue
@ -156,23 +180,23 @@ def render(context, content):
rendered += elem.render(context) + EOL
previous = elem
if isinstance(last, Content):
if isinstance(last, ContentItem):
rendered += last.end_block(context) + EOL
return rendered
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:
- 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
Return: a list of ContentItem objects, corresponding to the content to be
included in the .tex file.
"""
contentlist = []
contentlist = ContentList()
plugins = config.get('_content_plugins', {})
keyword_re = re.compile(r'^ *(?P<keyword>\w*) *(\((?P<argument>.*)\))? *$')
if not content:
@ -185,10 +209,12 @@ def process_content(content, config=None):
try:
match = keyword_re.match(elem[0]).groupdict()
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'])
if keyword not in plugins:
raise ContentError(keyword, "Unknown content type.")
contentlist.errors.append(ContentError(keyword, "Unknown content type."))
continue
contentlist.extend(plugins[keyword](
keyword,
argument=argument,

6
patacrep/content/section.py

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

12
patacrep/content/song.py

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

6
patacrep/content/songsection.py

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

6
patacrep/content/tex.py

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

11
patacrep/songs/chordpro/syntax.py

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

Loading…
Cancel
Save