diff --git a/patacrep/latex/__init__.py b/patacrep/latex/__init__.py index d596abae..3361191e 100644 --- a/patacrep/latex/__init__.py +++ b/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( - lang, - available - ) + 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] diff --git a/patacrep/songs/__init__.py b/patacrep/songs/__init__.py index 2b1a5768..55e56eac 100644 --- a/patacrep/songs/__init__.py +++ b/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 diff --git a/patacrep/songs/chordpro/__init__.py b/patacrep/songs/chordpro/__init__.py index 5777b170..c7c0bdf4 100644 --- a/patacrep/songs/chordpro/__init__.py +++ b/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, } diff --git a/patacrep/songs/chordpro/ast.py b/patacrep/songs/chordpro/ast.py index 8d287a79..380e6833 100644 --- a/patacrep/songs/chordpro/ast.py +++ b/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) diff --git a/patacrep/songs/chordpro/syntax.py b/patacrep/songs/chordpro/syntax.py index 999d31a5..2747d030 100644 --- a/patacrep/songs/chordpro/syntax.py +++ b/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 diff --git a/patacrep/songs/errors.py b/patacrep/songs/errors.py new file mode 100644 index 00000000..94aff3ed --- /dev/null +++ b/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" diff --git a/patacrep/songs/syntax.py b/patacrep/songs/syntax.py index ee7d95da..c7e933ec 100644 --- a/patacrep/songs/syntax.py +++ b/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), )