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 import logging
from collections import OrderedDict from collections import OrderedDict
from patacrep import errors
from patacrep.latex.syntax import tex2plain, parse_song from patacrep.latex.syntax import tex2plain, parse_song
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
@ -77,32 +78,57 @@ BABEL_LANGUAGES = OrderedDict((
# ('??_??', 'welsh'), # ('??_??', 'welsh'),
)) ))
def lang2babel(lang): class UnknownLanguage(Exception):
"""Return the language used by babel, corresponding to the language code""" """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 # Exact match
if lang.lower() in BABEL_LANGUAGES: if lang.lower() in BABEL_LANGUAGES:
return BABEL_LANGUAGES[lang.lower()] return lang.lower()
# Only language code is provided (e.g. 'fr') # Only language code is provided (e.g. 'fr')
for babel in BABEL_LANGUAGES: for babel in BABEL_LANGUAGES:
if babel.startswith(lang.lower()): if babel.startswith(lang.lower()):
return BABEL_LANGUAGES[babel] return babel
# A non existent country code is provided (e.g. 'fr_CD'). # A non existent country code is provided (e.g. 'fr_CD').
language = lang.lower().split("_")[0] language = lang.lower().split("_")[0]
for babel in BABEL_LANGUAGES: for babel in BABEL_LANGUAGES:
if babel.startswith(language): if babel.startswith(language):
LOGGER.error( raise UnknownLanguage(
"Unknown country code '{}'. Using default '{}' instead.".format( original=lang,
fallback=babel,
message="Unknown country code '{}'. Using default '{}' instead.".format(
lang, lang,
babel babel
) )
) )
return BABEL_LANGUAGES[babel]
# Error: no (exact or approximate) match found # Error: no (exact or approximate) match found
available = ", ".join(BABEL_LANGUAGES.keys()) available = ", ".join(BABEL_LANGUAGES.keys())
LOGGER.error( raise UnknownLanguage(
"Unknown language code '{}' (supported: {}). Using default 'english' instead.".format( original=lang,
fallback="en_us",
message=(
"Unknown language code '{}' (supported: {}). Using "
"default 'english' instead."
).format(
lang, lang,
available 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.encoding = config["encoding"]
self.default_lang = config["lang"] self.default_lang = config["lang"]
self.config = config self.config = config
self.errors = []
if self._cache_retrieved(): if self._cache_retrieved():
return return

1
patacrep/songs/chordpro/__init__.py

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

6
patacrep/songs/chordpro/ast.py

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

31
patacrep/songs/chordpro/syntax.py

@ -5,6 +5,7 @@ import ply.yacc as yacc
import re import re
from patacrep.songs.syntax import Parser from patacrep.songs.syntax import Parser
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
@ -42,7 +43,11 @@ class ChordproParser(Parser):
| empty | empty
""" """
if len(symbols) == 2: 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: else:
symbols[0] = symbols[2].add(symbols[1]) symbols[0] = symbols[2].add(symbols[1])
@ -140,24 +145,29 @@ class ChordproParser(Parser):
if match is None: if match is None:
if argument.strip(): if argument.strip():
self.error( error = errors.SongSyntaxError(
line=symbols.lexer.lineno, line=symbols.lexer.lineno,
message="Invalid chord definition '{}'.".format(argument), message="Invalid chord definition '{}'.".format(argument),
) )
self.error(line=error.line, message=error.message)
else: else:
self.error( error = errors.SongSyntaxError(
line=symbols.lexer.lineno, line=symbols.lexer.lineno,
message="Invalid empty chord definition.", message="Invalid empty chord definition.",
) )
self.error(line=error.line, message=error.message)
self._errors.append(error)
symbols[0] = ast.Error() symbols[0] = ast.Error()
return return
define = self._parse_define(match.groupdict()) define = self._parse_define(match.groupdict())
if define is None: if define is None:
self.error( error = errors.SongSyntaxError(
line=symbols.lexer.lineno, line=symbols.lexer.lineno,
message="Invalid chord definition '{}'.".format(argument), message="Invalid chord definition '{}'.".format(argument),
) )
self.error(line=error.line, message=error.message)
self._errors.append(error)
symbols[0] = ast.Error() symbols[0] = ast.Error()
return return
self._directives.append(define) self._directives.append(define)
@ -186,10 +196,14 @@ class ChordproParser(Parser):
else: else:
symbols[0] = None symbols[0] = None
@staticmethod def p_line_error(self, symbols):
def p_line_error(symbols):
"""line_error : error directive""" """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() symbols[0] = ast.Line()
@staticmethod @staticmethod
@ -321,5 +335,8 @@ 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:
# 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)) raise SyntaxError('Fatal error during song parsing: {}'.format(filename))
return parsed_content 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 import logging
from patacrep.songs import errors
LOGGER = logging.getLogger() LOGGER = logging.getLogger()
class Parser: class Parser:
@ -10,6 +12,7 @@ class Parser:
def __init__(self): def __init__(self):
self.filename = "" # Will be overloaded self.filename = "" # Will be overloaded
self._errors = []
@staticmethod @staticmethod
def __find_column(token): def __find_column(token):
@ -42,11 +45,17 @@ class Parser:
def p_error(self, token): def p_error(self, token):
"""Manage parsing errors.""" """Manage parsing errors."""
if token is None: if token is None:
self.error( error = errors.SongSyntaxError(
line=None,
message="Unexpected end of file.", message="Unexpected end of file.",
) )
self.error(message=error.message)
else: else:
self.error( error = errors.SongSyntaxError(
line=token.lineno, line=token.lineno,
message="Syntax error",
)
self.error(
line=error.line,
column=self.__find_column(token), column=self.__find_column(token),
) )

Loading…
Cancel
Save