Browse Source

Add an `errors` attribute to songs, compiling the song errors

First step to #121
pull/176/head
Louis 9 years ago
parent
commit
f8c84de29c
  1. 46
      patacrep/latex/__init__.py
  2. 1
      patacrep/songs/__init__.py
  3. 1
      patacrep/songs/chordpro/__init__.py
  4. 6
      patacrep/songs/chordpro/ast.py
  5. 31
      patacrep/songs/chordpro/syntax.py
  6. 33
      patacrep/songs/errors.py
  7. 13
      patacrep/songs/syntax.py

46
patacrep/latex/__init__.py

@ -8,6 +8,7 @@ will work on simple cases, but not on complex ones.
import logging
from collections import OrderedDict
from patacrep import errors
from patacrep.latex.syntax import tex2plain, parse_song
LOGGER = logging.getLogger(__name__)
@ -77,32 +78,57 @@ BABEL_LANGUAGES = OrderedDict((
# ('??_??', 'welsh'),
))
def lang2babel(lang):
"""Return the language used by babel, corresponding to the language code"""
class UnknownLanguage(Exception):
"""Error: Unknown language."""
def __init__(self, *, original, fallback, message):
super().__init__()
self.original = original
self.fallback = fallback
self.message = message
def checklanguage(lang):
"""Check that `lang` is a known language.
Raise an :class:`UnknownLanguage` excetipn if not.
"""
# Exact match
if lang.lower() in BABEL_LANGUAGES:
return BABEL_LANGUAGES[lang.lower()]
return lang.lower()
# Only language code is provided (e.g. 'fr')
for babel in BABEL_LANGUAGES:
if babel.startswith(lang.lower()):
return BABEL_LANGUAGES[babel]
return babel
# A non existent country code is provided (e.g. 'fr_CD').
language = lang.lower().split("_")[0]
for babel in BABEL_LANGUAGES:
if babel.startswith(language):
LOGGER.error(
"Unknown country code '{}'. Using default '{}' instead.".format(
raise UnknownLanguage(
original=lang,
fallback=babel,
message="Unknown country code '{}'. Using default '{}' instead.".format(
lang,
babel
)
)
return BABEL_LANGUAGES[babel]
# Error: no (exact or approximate) match found
available = ", ".join(BABEL_LANGUAGES.keys())
LOGGER.error(
"Unknown language code '{}' (supported: {}). Using default 'english' instead.".format(
raise UnknownLanguage(
original=lang,
fallback="en_us",
message=(
"Unknown language code '{}' (supported: {}). Using "
"default 'english' instead."
).format(
lang,
available
)
)
return 'english'
def lang2babel(lang):
"""Return the language used by babel, corresponding to the language code"""
try:
return BABEL_LANGUAGES[checklanguage(lang)]
except UnknownLanguage as error:
LOGGER.error(str(error))
return BABEL_LANGUAGES[error.fallback]

1
patacrep/songs/__init__.py

@ -108,6 +108,7 @@ class Song:
self.encoding = config["encoding"]
self.default_lang = config["lang"]
self.config = config
self.errors = []
if self._cache_retrieved():
return

1
patacrep/songs/chordpro/__init__.py

@ -33,6 +33,7 @@ class ChordproSong(Song):
self.titles = song.titles
self.lang = song.get_data_argument('language', self.default_lang)
self.data = song.meta
self.errors = song.errors
self.cached = {
'song': song,
}

6
patacrep/songs/chordpro/ast.py

@ -223,7 +223,7 @@ class Song(AST):
"tag": "add_cumulative",
}
def __init__(self, filename, directives):
def __init__(self, filename, directives, *, errors=None):
super().__init__()
self.content = []
self.meta = OrderedDict()
@ -231,6 +231,10 @@ class Song(AST):
self._titles = []
self._subtitles = []
self.filename = filename
if errors is None:
self.errors = []
else:
self.errors = errors
for directive in directives:
self.add(directive)

31
patacrep/songs/chordpro/syntax.py

@ -5,6 +5,7 @@ import ply.yacc as yacc
import re
from patacrep.songs.syntax import Parser
from patacrep.songs import errors
from patacrep.songs.chordpro import ast
from patacrep.songs.chordpro.lexer import tokens, ChordProLexer
@ -42,7 +43,11 @@ class ChordproParser(Parser):
| empty
"""
if len(symbols) == 2:
symbols[0] = ast.Song(self.filename, self._directives)
symbols[0] = ast.Song(
self.filename,
directives=self._directives,
errors=self._errors,
)
else:
symbols[0] = symbols[2].add(symbols[1])
@ -140,24 +145,29 @@ class ChordproParser(Parser):
if match is None:
if argument.strip():
self.error(
error = errors.SongSyntaxError(
line=symbols.lexer.lineno,
message="Invalid chord definition '{}'.".format(argument),
)
self.error(line=error.line, message=error.message)
else:
self.error(
error = errors.SongSyntaxError(
line=symbols.lexer.lineno,
message="Invalid empty chord definition.",
)
self.error(line=error.line, message=error.message)
self._errors.append(error)
symbols[0] = ast.Error()
return
define = self._parse_define(match.groupdict())
if define is None:
self.error(
error = errors.SongSyntaxError(
line=symbols.lexer.lineno,
message="Invalid chord definition '{}'.".format(argument),
)
self.error(line=error.line, message=error.message)
self._errors.append(error)
symbols[0] = ast.Error()
return
self._directives.append(define)
@ -186,10 +196,14 @@ class ChordproParser(Parser):
else:
symbols[0] = None
@staticmethod
def p_line_error(symbols):
def p_line_error(self, symbols):
"""line_error : error directive"""
LOGGER.error("Directive can only be preceded or followed by spaces")
error = errors.SongSyntaxError(
line=symbols.lexer.lineno,
message="Directive can only be preceded or followed by spaces",
)
self._errors.append(error)
LOGGER.error(error.message)
symbols[0] = ast.Line()
@staticmethod
@ -321,5 +335,8 @@ def parse_song(content, filename=None):
lexer=ChordProLexer(filename=filename).lexer,
)
if parsed_content is None:
# pylint: disable=fixme
# TODO: Provide a nicer error (an empty song?)
# TODO: Add an error to the song.errors list.
raise SyntaxError('Fatal error during song parsing: {}'.format(filename))
return parsed_content

33
patacrep/songs/errors.py

@ -0,0 +1,33 @@
"""Errors in song definition (syntax errors, and so on)"""
class SongError:
"""Generic song error"""
# pylint: disable=too-few-public-methods
type = "generic"
def __init__(self, message):
self.message = message
def __str__(self):
raise NotImplementedError()
class SongSyntaxError(SongError):
"""Syntax error"""
# pylint: disable=too-few-public-methods
type = "syntax"
def __init__(self, line, message):
super().__init__(message)
#: Line of error. May be `None` if irrelevant.
self.line = line
def __str__(self):
return "Line {}: {}".format(self.line, self.message)
# class FileError(SongError):
# type = "file"
#
# class LanguageError(SongError):
# type = "language"

13
patacrep/songs/syntax.py

@ -2,6 +2,8 @@
import logging
from patacrep.songs import errors
LOGGER = logging.getLogger()
class Parser:
@ -10,6 +12,7 @@ class Parser:
def __init__(self):
self.filename = "" # Will be overloaded
self._errors = []
@staticmethod
def __find_column(token):
@ -42,11 +45,17 @@ class Parser:
def p_error(self, token):
"""Manage parsing errors."""
if token is None:
self.error(
error = errors.SongSyntaxError(
line=None,
message="Unexpected end of file.",
)
self.error(message=error.message)
else:
self.error(
error = errors.SongSyntaxError(
line=token.lineno,
message="Syntax error",
)
self.error(
line=error.line,
column=self.__find_column(token),
)

Loading…
Cancel
Save