From a09d3f5256020d5bc1d9935d81213d53853d1abd Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 11 Apr 2015 18:48:19 +0200 Subject: [PATCH] Added a pylintrc file; made code pylint compliant --- patacrep/authors.py | 58 +++++----- patacrep/build.py | 84 +++++++-------- patacrep/content/cwd.py | 8 +- patacrep/content/include.py | 12 +-- patacrep/content/section.py | 24 ++--- patacrep/content/song.py | 27 ++--- patacrep/content/songsection.py | 12 +-- patacrep/content/sorted.py | 8 +- patacrep/content/tex.py | 10 +- patacrep/encoding.py | 10 +- patacrep/errors.py | 29 +++-- patacrep/files.py | 30 +++--- patacrep/index.py | 34 +++--- patacrep/latex/ast.py | 12 +-- patacrep/latex/detex.py | 8 +- patacrep/latex/lexer.py | 54 +++++----- patacrep/latex/syntax.py | 39 ++----- patacrep/songbook.py | 111 +++++++++++--------- patacrep/songs/__init__.py | 54 +++++----- patacrep/songs/chordpro/__init__.py | 16 +-- patacrep/songs/chordpro/ast.py | 36 +++---- patacrep/songs/chordpro/lexer.py | 2 +- patacrep/songs/chordpro/syntax.py | 50 ++------- patacrep/songs/chordpro/test/test_parser.py | 4 +- patacrep/songs/latex/__init__.py | 8 +- patacrep/songs/syntax.py | 33 ++++++ patacrep/templates.py | 69 ++++++------ pylintrc | 5 + 28 files changed, 426 insertions(+), 421 deletions(-) create mode 100644 patacrep/songs/syntax.py create mode 100644 pylintrc diff --git a/patacrep/authors.py b/patacrep/authors.py index 84613a1a..db610f60 100644 --- a/patacrep/authors.py +++ b/patacrep/authors.py @@ -6,10 +6,10 @@ import re LOGGER = logging.getLogger(__name__) DEFAULT_AUTHWORDS = { - "after": ["by"], - "ignore": ["unknown"], - "sep": ["and"], - } + "after": ["by"], + "ignore": ["unknown"], + "sep": ["and"], + } def compile_authwords(authwords): """Convert strings of authwords to compiled regular expressions. @@ -23,13 +23,13 @@ def compile_authwords(authwords): # Compilation authwords['after'] = [ - re.compile(r"^.*\b%s\b(.*)$" % word, re.LOCALE) - for word in authwords['after'] - ] + re.compile(r"^.*\b%s\b(.*)$" % word, re.LOCALE) + for word in authwords['after'] + ] authwords['sep'] = [ - re.compile(r"^(.*)%s +(.*)$" % word, re.LOCALE) - for word in ([" %s" % word for word in authwords['sep']] + [',']) - ] + re.compile(r"^(.*)%s +(.*)$" % word, re.LOCALE) + for word in ([" %s" % word for word in authwords['sep']] + [',']) + ] return authwords @@ -153,11 +153,11 @@ def processauthors_clean_authors(authors_list): See docstring of processauthors() for more information. """ return [ - author.lstrip() - for author - in authors_list - if author.lstrip() - ] + author.lstrip() + for author + in authors_list + if author.lstrip() + ] def processauthors(authors_string, after=None, ignore=None, sep=None): r"""Return a list of authors @@ -210,20 +210,20 @@ def processauthors(authors_string, after=None, ignore=None, sep=None): ignore = [] return [ - split_author_names(author) - for author - in processauthors_clean_authors( - processauthors_ignore_authors( - processauthors_remove_after( - processauthors_split_string( - processauthors_removeparen( - authors_string - ), - sep), - after), - ignore) - ) - ] + split_author_names(author) + for author + in processauthors_clean_authors( + processauthors_ignore_authors( + processauthors_remove_after( + processauthors_split_string( + processauthors_removeparen( + authors_string + ), + sep), + after), + ignore) + ) + ] def process_listauthors(authors_list, after=None, ignore=None, sep=None): """Process a list of authors, and return the list of resulting authors.""" diff --git a/patacrep/build.py b/patacrep/build.py index a575f9a4..c9aa87e3 100644 --- a/patacrep/build.py +++ b/patacrep/build.py @@ -17,23 +17,23 @@ LOGGER = logging.getLogger(__name__) EOL = "\n" DEFAULT_STEPS = ['tex', 'pdf', 'sbx', 'pdf', 'clean'] GENERATED_EXTENSIONS = [ - "_auth.sbx", - "_auth.sxd", - ".aux", - ".log", - ".out", - ".sxc", - ".tex", - "_title.sbx", - "_title.sxd", - ] + "_auth.sbx", + "_auth.sxd", + ".aux", + ".log", + ".out", + ".sxc", + ".tex", + "_title.sbx", + "_title.sxd", + ] DEFAULT_CONFIG = { - 'template': "default.tex", - 'lang': 'english', - 'content': [], - 'titleprefixwords': [], - 'encoding': None, - } + 'template': "default.tex", + 'lang': 'english', + 'content': [], + 'titleprefixwords': [], + 'encoding': None, + } @@ -67,16 +67,16 @@ class Songbook(object): abs_datadir.append(os.path.abspath(path)) else: LOGGER.warning( - "Ignoring non-existent datadir '{}'.".format(path) - ) + "Ignoring non-existent datadir '{}'.".format(path) + ) abs_datadir.append(__DATADIR__) self.config['datadir'] = abs_datadir self.config['_songdir'] = [ - DataSubpath(path, 'songs') - for path in self.config['datadir'] - ] + DataSubpath(path, 'songs') + for path in self.config['datadir'] + ] def write_tex(self, output): """Build the '.tex' file corresponding to self. @@ -88,17 +88,17 @@ class Songbook(object): config = DEFAULT_CONFIG.copy() config.update(self.config) renderer = TexBookRenderer( - config['template'], - config['datadir'], - config['lang'], - config['encoding'], - ) + config['template'], + config['datadir'], + config['lang'], + config['encoding'], + ) config.update(renderer.get_variables()) config.update(self.config) config['_compiled_authwords'] = authors.compile_authwords( - copy.deepcopy(config['authwords']) - ) + copy.deepcopy(config['authwords']) + ) # Loading custom plugins config['_content_plugins'] = files.load_plugins( @@ -115,9 +115,9 @@ class Songbook(object): # Configuration set config['render_content'] = content.render_content config['content'] = content.process_content( - config.get('content', []), - config, - ) + config.get('content', []), + config, + ) config['filename'] = output.name[:-4] renderer.render_tex(output, config) @@ -166,8 +166,8 @@ class SongbookBuilder(object): self._pdflatex_options.append("-halt-on-error") for datadir in self.songbook.config["datadir"]: self._pdflatex_options.append( - '--include-directory="{}"'.format(datadir) - ) + '--include-directory="{}"'.format(datadir) + ) def build_steps(self, steps=None): """Perform steps on the songbook by calling relevant self.build_*() @@ -204,8 +204,8 @@ class SongbookBuilder(object): """Build .tex file from templates""" LOGGER.info("Building '{}.tex'…".format(self.basename)) with codecs.open( - "{}.tex".format(self.basename), 'w', 'utf-8', - ) as output: + "{}.tex".format(self.basename), 'w', 'utf-8', + ) as output: self.songbook.write_tex(output) def build_pdf(self): @@ -215,13 +215,13 @@ class SongbookBuilder(object): try: process = Popen( - ["pdflatex"] + self._pdflatex_options + [self.basename], - stdin=PIPE, - stdout=PIPE, - stderr=PIPE, - env=os.environ, - universal_newlines=True, - ) + ["pdflatex"] + self._pdflatex_options + [self.basename], + stdin=PIPE, + stdout=PIPE, + stderr=PIPE, + env=os.environ, + universal_newlines=True, + ) except Exception as error: LOGGER.debug(error) raise errors.LatexCompilationError(self.basename) diff --git a/patacrep/content/cwd.py b/patacrep/content/cwd.py index 67468761..b4d9c2de 100755 --- a/patacrep/content/cwd.py +++ b/patacrep/content/cwd.py @@ -24,10 +24,10 @@ def parse(keyword, config, argument, contentlist): """ old_songdir = config['_songdir'] config['_songdir'] = ( - [DataSubpath("", argument)] + - [path.clone().join(argument) for path in config['_songdir']] + - config['_songdir'] - ) + [DataSubpath("", argument)] + + [path.clone().join(argument) for path in config['_songdir']] + + config['_songdir'] + ) processed_content = process_content(contentlist, config) config['_songdir'] = old_songdir return processed_content diff --git a/patacrep/content/include.py b/patacrep/content/include.py index 43c79b95..73bfa82a 100644 --- a/patacrep/content/include.py +++ b/patacrep/content/include.py @@ -26,9 +26,9 @@ def load_from_datadirs(path, config=None): return filepath # File not found raise ContentError( - "include", - errors.notfound(path, config.get("datadir", [])), - ) + "include", + errors.notfound(path, config.get("datadir", [])), + ) #pylint: disable=unused-argument def parse(keyword, config, argument, contentlist): @@ -47,9 +47,9 @@ def parse(keyword, config, argument, contentlist): content_file = None try: with encoding.open_read( - filepath, - encoding=config['encoding'] - ) as content_file: + filepath, + encoding=config['encoding'] + ) as content_file: new_content = json.load(content_file) except Exception as error: # pylint: disable=broad-except LOGGER.error(error) diff --git a/patacrep/content/section.py b/patacrep/content/section.py index 1aec02ad..e2ecfd32 100755 --- a/patacrep/content/section.py +++ b/patacrep/content/section.py @@ -3,14 +3,14 @@ from patacrep.content import Content, ContentError KEYWORDS = [ - "part", - "chapter", - "section", - "subsection", - "subsubsection", - "paragraph", - "subparagraph", - ] + "part", + "chapter", + "section", + "subsection", + "subsubsection", + "paragraph", + "subparagraph", + ] FULL_KEYWORDS = KEYWORDS + ["{}*".format(word) for word in KEYWORDS] class Section(Content): @@ -43,12 +43,12 @@ def parse(keyword, argument, contentlist, config): """ if (keyword not in KEYWORDS) and (len(contentlist) != 1): raise ContentError( - keyword, - "Starred section names must have exactly one argument." - ) + keyword, + "Starred section names must have exactly one argument." + ) if (len(contentlist) not in [1, 2]): raise ContentError(keyword, "Section can have one or two arguments.") - return [Section(keyword, *contentlist)] #pylint: disable=star-args + return [Section(keyword, *contentlist)] CONTENT_PLUGINS = dict([ diff --git a/patacrep/content/song.py b/patacrep/content/song.py index 9ac90861..f252e314 100755 --- a/patacrep/content/song.py +++ b/patacrep/content/song.py @@ -67,7 +67,8 @@ def parse(keyword, argument, contentlist, config): LOGGER.debug('Parsing file "{}"…'.format(filename)) extension = filename.split(".")[-1] if extension not in plugins: - LOGGER.warning(( + LOGGER.warning( + ( 'I do not know how to parse "{}": name does ' 'not end with one of {}. Ignored.' ).format( @@ -76,10 +77,10 @@ def parse(keyword, argument, contentlist, config): )) continue renderer = SongRenderer(plugins[extension]( - songdir.datadir, - filename, - config, - )) + songdir.datadir, + filename, + config, + )) songlist.append(renderer) config["_languages"].update(renderer.song.languages) if len(songlist) > before: @@ -105,9 +106,9 @@ class OnlySongsError(ContentError): def __str__(self): return ( - "Only songs are allowed, and the following items are not:" + - str(self.not_songs) - ) + "Only songs are allowed, and the following items are not:" + + str(self.not_songs) + ) def process_songs(content, config=None): """Process content that containt only songs. @@ -117,11 +118,11 @@ def process_songs(content, config=None): """ contentlist = process_content(content, config) not_songs = [ - item - for item - in contentlist - if not isinstance(item, SongRenderer) - ] + item + for item + in contentlist + if not isinstance(item, SongRenderer) + ] if not_songs: raise OnlySongsError(not_songs) return contentlist diff --git a/patacrep/content/songsection.py b/patacrep/content/songsection.py index c559d17c..fc185b57 100755 --- a/patacrep/content/songsection.py +++ b/patacrep/content/songsection.py @@ -3,9 +3,9 @@ from patacrep.content import Content, ContentError KEYWORDS = [ - "songchapter", - "songsection", - ] + "songchapter", + "songsection", + ] class SongSection(Content): """A songsection or songchapter.""" @@ -31,9 +31,9 @@ def parse(keyword, argument, contentlist, config): """ if (keyword not in KEYWORDS) and (len(contentlist) != 1): raise ContentError( - keyword, - "Starred section names must have exactly one argument.", - ) + keyword, + "Starred section names must have exactly one argument.", + ) return [SongSection(keyword, contentlist[0])] diff --git a/patacrep/content/sorted.py b/patacrep/content/sorted.py index b3bbe40a..4fac210d 100755 --- a/patacrep/content/sorted.py +++ b/patacrep/content/sorted.py @@ -56,11 +56,11 @@ def key_generator(sort): field = song.data[key] except KeyError: LOGGER.debug( - "Ignoring unknown key '{}' for song {}.".format( - key, - files.relpath(song.fullpath), - ) + "Ignoring unknown key '{}' for song {}.".format( + key, + files.relpath(song.fullpath), ) + ) field = "" songkey.append(normalize_field(field)) return songkey diff --git a/patacrep/content/tex.py b/patacrep/content/tex.py index 9a8227d3..8bb7236d 100755 --- a/patacrep/content/tex.py +++ b/patacrep/content/tex.py @@ -32,8 +32,8 @@ def parse(keyword, argument, contentlist, config): """ if not contentlist: LOGGER.warning( - "Useless 'tex' content: list of files to include is empty." - ) + "Useless 'tex' content: list of files to include is empty." + ) filelist = [] basefolders = [path.fullpath for path in config['_songdir']] +\ config['datadir'] + \ @@ -48,9 +48,11 @@ def parse(keyword, argument, contentlist, config): )) break if not checked_file: - LOGGER.warning("{} Compilation may fail later.".format( - errors.notfound(filename, basefolders)) + LOGGER.warning( + "{} Compilation may fail later.".format( + errors.notfound(filename, basefolders) ) + ) continue filelist.append(LaTeX(checked_file)) diff --git a/patacrep/encoding.py b/patacrep/encoding.py index bd36b560..b8bdc8e6 100644 --- a/patacrep/encoding.py +++ b/patacrep/encoding.py @@ -21,9 +21,9 @@ def open_read(filename, mode='r', encoding=None): fileencoding = encoding with codecs.open( - filename, - mode=mode, - encoding=fileencoding, - errors='replace', - ) as fileobject: + filename, + mode=mode, + encoding=fileencoding, + errors='replace', + ) as fileobject: yield fileobject diff --git a/patacrep/errors.py b/patacrep/errors.py index f6e4121a..7ecd7389 100644 --- a/patacrep/errors.py +++ b/patacrep/errors.py @@ -39,9 +39,10 @@ class LatexCompilationError(SongbookError): self.basename = basename def __str__(self): - return ("""Error while pdfLaTeX compilation of "{basename}.tex" """ - """(see {basename}.log for more information).""" - ).format(basename=self.basename) + return ( + """Error while pdfLaTeX compilation of "{basename}.tex" """ + """(see {basename}.log for more information).""" + ).format(basename=self.basename) class StepCommandError(SongbookError): """Error during custom command compilation.""" @@ -65,9 +66,9 @@ class CleaningError(SongbookError): def __str__(self): return """Error while removing "{filename}": {exception}.""".format( - filename=self.filename, - exception=str(self.exception) - ) + filename=self.filename, + exception=str(self.exception) + ) class UnknownStep(SongbookError): """Unknown compilation step.""" @@ -79,6 +80,16 @@ class UnknownStep(SongbookError): def __str__(self): return """Compilation step "{step}" unknown.""".format(step=self.step) +class ParsingError(SongbookError): + """Parsing error.""" + + def __init__(self, message): + super().__init__(self) + self.message = message + + def __str__(self): + return self.message + def notfound(filename, paths, message=None): """Return a string saying that file was not found in paths.""" if message is None: @@ -87,6 +98,6 @@ def notfound(filename, paths, message=None): #pylint: disable=expression-not-assigned [unique_paths.append(item) for item in paths if item not in unique_paths] return message.format( - name=filename, - paths=", ".join(['"{}"'.format(item) for item in unique_paths]), - ) + name=filename, + paths=", ".join(['"{}"'.format(item) for item in unique_paths]), + ) diff --git a/patacrep/files.py b/patacrep/files.py index 5048f805..17232e4f 100644 --- a/patacrep/files.py +++ b/patacrep/files.py @@ -49,8 +49,9 @@ def path2posix(string): return string[0:-1] (head, tail) = os.path.split(string) return posixpath.join( - path2posix(head), - tail) + path2posix(head), + tail, + ) @contextmanager def chdir(path): @@ -87,24 +88,23 @@ def load_plugins(datadirs, root_modules, keyword): - keys are the keywords ; - values are functions triggered when this keyword is met. """ - # pylint: disable=star-args plugins = {} directory_list = ( - [ - os.path.join(datadir, "python", *root_modules) - for datadir in datadirs - ] - + [os.path.join( - os.path.dirname(__file__), - *root_modules - )] - ) + [ + os.path.join(datadir, "python", *root_modules) + for datadir in datadirs + ] + + [os.path.join( + os.path.dirname(__file__), + *root_modules + )] + ) for directory in directory_list: if not os.path.exists(directory): LOGGER.debug( - "Ignoring non-existent directory '%s'.", - directory - ) + "Ignoring non-existent directory '%s'.", + directory + ) continue for (dirpath, __ignored, filenames) in os.walk(directory): modules = ["patacrep"] + root_modules diff --git a/patacrep/index.py b/patacrep/index.py index 8540d433..b67897f3 100644 --- a/patacrep/index.py +++ b/patacrep/index.py @@ -105,12 +105,12 @@ class Index(object): self.data[first] = dict() if not key in self.data[first]: self.data[first][key] = { - 'sortingkey': [ - unidecode.unidecode(tex2plain(item)).lower() - for item in key - ], - 'entries': [], - } + 'sortingkey': [ + unidecode.unidecode(tex2plain(item)).lower() + for item in key + ], + 'entries': [], + } self.data[first][key]['entries'].append({'num': number, 'link': link}) def add(self, key, number, link): @@ -124,13 +124,13 @@ class Index(object): match = pattern.match(key) if match: self._raw_add( - ( - (match.group(2) + match.group(3)).strip(), - match.group(1).strip(), - ), - number, - link - ) + ( + (match.group(2) + match.group(3)).strip(), + match.group(1).strip(), + ), + number, + link + ) return self._raw_add((key, ""), number, link) @@ -179,10 +179,10 @@ class Index(object): def sortkey(key): """Return something sortable for `entries[key]`.""" return [ - locale.strxfrm(item) - for item - in entries[key]['sortingkey'] - ] + locale.strxfrm(item) + for item + in entries[key]['sortingkey'] + ] string = r'\begin{idxblock}{' + letter + '}' + EOL for key in sorted(entries, key=sortkey): string += " " + self.entry_to_str(key, entries[key]['entries']) diff --git a/patacrep/latex/ast.py b/patacrep/latex/ast.py index 798d4c33..c4763ddf 100644 --- a/patacrep/latex/ast.py +++ b/patacrep/latex/ast.py @@ -16,8 +16,8 @@ class AST: parsing. """ cls.metadata = { - '@languages': set(), - } + '@languages': set(), + } class Expression(AST): """LaTeX expression""" @@ -50,10 +50,10 @@ class Command(AST): if self.name in [r'\emph']: return str(self.mandatory[0]) return "{}{}{}".format( - self.name, - "".join(["[{}]".format(item) for item in self.optional]), - "".join(["{{{}}}".format(item) for item in self.mandatory]), - ) + self.name, + "".join(["[{}]".format(item) for item in self.optional]), + "".join(["{{{}}}".format(item) for item in self.mandatory]), + ) class BeginSong(AST): diff --git a/patacrep/latex/detex.py b/patacrep/latex/detex.py index ebfd721b..0cc57c9d 100644 --- a/patacrep/latex/detex.py +++ b/patacrep/latex/detex.py @@ -105,10 +105,10 @@ def detex(arg): ]) elif isinstance(arg, list): return [ - detex(item) - for item - in arg - ] + detex(item) + for item + in arg + ] elif isinstance(arg, set): return set(detex(list(arg))) elif isinstance(arg, str): diff --git a/patacrep/latex/lexer.py b/patacrep/latex/lexer.py index 8954e14c..b8a762c0 100644 --- a/patacrep/latex/lexer.py +++ b/patacrep/latex/lexer.py @@ -7,21 +7,21 @@ LOGGER = logging.getLogger() #pylint: disable=invalid-name tokens = ( - 'LBRACKET', - 'RBRACKET', - 'LBRACE', - 'RBRACE', - 'COMMAND', - 'NEWLINE', - 'COMMA', - 'EQUAL', - 'CHARACTER', - 'SPACE', - 'BEGINSONG', - 'SONG_LTITLE', - 'SONG_RTITLE', - 'SONG_LOPTIONS', - 'SONG_ROPTIONS', + 'LBRACKET', + 'RBRACKET', + 'LBRACE', + 'RBRACE', + 'COMMAND', + 'NEWLINE', + 'COMMA', + 'EQUAL', + 'CHARACTER', + 'SPACE', + 'BEGINSONG', + 'SONG_LTITLE', + 'SONG_RTITLE', + 'SONG_LOPTIONS', + 'SONG_ROPTIONS', ) class SimpleLexer: @@ -36,18 +36,18 @@ class SimpleLexer: t_COMMAND = r'\\([@a-zA-Z]+|[^\\])' t_NEWLINE = r'\\\\' SPECIAL_CHARACTERS = ( - t_LBRACKET + - t_RBRACKET + - t_RBRACE + - t_LBRACE + - r"\\" + - r" " + - r"\n" + - r"\r" + - r"%" + - r"=" + - r"," - ) + t_LBRACKET + + t_RBRACKET + + t_RBRACE + + t_LBRACE + + r"\\" + + r" " + + r"\n" + + r"\r" + + r"%" + + r"=" + + r"," + ) t_CHARACTER = r'[^{}]'.format(SPECIAL_CHARACTERS) t_EQUAL = r'=' t_COMMA = r',' diff --git a/patacrep/latex/syntax.py b/patacrep/latex/syntax.py index aff3a885..46c42ea1 100644 --- a/patacrep/latex/syntax.py +++ b/patacrep/latex/syntax.py @@ -3,52 +3,25 @@ import logging import ply.yacc as yacc +from patacrep.songs.syntax import Parser from patacrep.latex.lexer import tokens, SimpleLexer, SongLexer from patacrep.latex import ast -from patacrep.errors import SongbookError +from patacrep.errors import ParsingError from patacrep.latex.detex import detex LOGGER = logging.getLogger() -class ParsingError(SongbookError): - """Parsing error.""" - - def __init__(self, message): - super().__init__(self) - self.message = message - - def __str__(self): - return self.message - # pylint: disable=line-too-long -class Parser: +class LatexParser(Parser): """LaTeX parser.""" def __init__(self, filename=None): + super().__init__() self.tokens = tokens self.ast = ast.AST self.ast.init_metadata() self.filename = filename - @staticmethod - def __find_column(token): - """Return the column of ``token``.""" - last_cr = token.lexer.lexdata.rfind('\n', 0, token.lexpos) - if last_cr < 0: - last_cr = 0 - column = (token.lexpos - last_cr) + 1 - return column - - def p_error(self, token): - """Manage parsing errors.""" - LOGGER.error( - "Error in file {}, line {} at position {}.".format( - str(self.filename), - token.lineno, - self.__find_column(token), - ) - ) - @staticmethod def p_expression(symbols): """expression : brackets expression @@ -238,7 +211,7 @@ def tex2plain(string): """Parse string and return its plain text version.""" return detex( silent_yacc( - module=Parser(), + module=LatexParser(), ).parse( string, lexer=SimpleLexer().lexer, @@ -254,7 +227,7 @@ def parse_song(content, filename=None): display error messages. """ return detex( - silent_yacc(module=Parser(filename)).parse( + silent_yacc(module=LatexParser(filename)).parse( content, lexer=SongLexer().lexer, ).metadata diff --git a/patacrep/songbook.py b/patacrep/songbook.py index 45a32560..219d532b 100755 --- a/patacrep/songbook.py +++ b/patacrep/songbook.py @@ -24,13 +24,13 @@ class ParseStepsAction(argparse.Action): if not getattr(namespace, self.dest): setattr(namespace, self.dest, []) setattr( - namespace, - self.dest, - ( - getattr(namespace, self.dest) - + [value.strip() for value in values[0].split(',')] - ), - ) + namespace, + self.dest, + ( + getattr(namespace, self.dest) + + [value.strip() for value in values[0].split(',')] + ), + ) class VerboseAction(argparse.Action): """Set verbosity level with option --verbose.""" @@ -41,40 +41,47 @@ def argument_parser(args): """Parse arguments""" parser = argparse.ArgumentParser(description="A song book compiler") - parser.add_argument('--version', help='Show version', action='version', - version='%(prog)s ' + __version__) - - parser.add_argument('book', nargs=1, help=textwrap.dedent("""\ - Book to compile. - """)) - - parser.add_argument('--datadir', '-d', nargs='+', type=str, action='append', - help=textwrap.dedent("""\ - Data location. Expected (not necessarily required) - subdirectories are 'songs', 'img', 'latex', 'templates'. - """)) - - parser.add_argument('--verbose', '-v', nargs=0, action=VerboseAction, - help=textwrap.dedent("""\ - Show details about the compilation process. - """)) - - parser.add_argument('--steps', '-s', nargs=1, type=str, - action=ParseStepsAction, - help=textwrap.dedent("""\ - Steps to run. Default is "{steps}". - Available steps are: - "tex" produce .tex file from templates; - "pdf" compile .tex file; - "sbx" compile index files; - "clean" remove temporary files; - any string beginning with '%%' (in this case, it will be run - in a shell). Several steps (excepted the custom shell - command) can be combinend in one --steps argument, as a - comma separated string. - """.format(steps=','.join(DEFAULT_STEPS))), - default=None, - ) + parser.add_argument( + '--version', help='Show version', action='version', + version='%(prog)s ' + __version__, + ) + + parser.add_argument( + 'book', nargs=1, help=textwrap.dedent("Book to compile.") + ) + + parser.add_argument( + '--datadir', '-d', nargs='+', type=str, action='append', + help=textwrap.dedent("""\ + Data location. Expected (not necessarily required) + subdirectories are 'songs', 'img', 'latex', 'templates'. + """) + ) + + parser.add_argument( + '--verbose', '-v', nargs=0, action=VerboseAction, + help=textwrap.dedent("""\ + Show details about the compilation process. + """) + ) + + parser.add_argument( + '--steps', '-s', nargs=1, type=str, + action=ParseStepsAction, + help=textwrap.dedent("""\ + Steps to run. Default is "{steps}". + Available steps are: + "tex" produce .tex file from templates; + "pdf" compile .tex file; + "sbx" compile index files; + "clean" remove temporary files; + any string beginning with '%%' (in this case, it will be run + in a shell). Several steps (excepted the custom shell + command) can be combinend in one --steps argument, as a + comma separated string. + """.format(steps=','.join(DEFAULT_STEPS))), + default=None, + ) options = parser.parse_args(args) @@ -102,9 +109,9 @@ def main(): songbook = json.load(songbook_file) if 'encoding' in songbook: with patacrep.encoding.open_read( - songbook_path, - encoding=songbook['encoding'] - ) as songbook_file: + songbook_path, + encoding=songbook['encoding'] + ) as songbook_file: songbook = json.load(songbook_file) except Exception as error: # pylint: disable=broad-except LOGGER.error(error) @@ -121,12 +128,12 @@ def main(): if isinstance(songbook['datadir'], str): songbook['datadir'] = [songbook['datadir']] datadirs += [ - os.path.join( - os.path.dirname(os.path.abspath(songbook_path)), - path - ) - for path in songbook['datadir'] - ] + os.path.join( + os.path.dirname(os.path.abspath(songbook_path)), + path + ) + for path in songbook['datadir'] + ] # Default value datadirs.append(os.path.dirname(os.path.abspath(songbook_path))) @@ -141,8 +148,8 @@ def main(): LOGGER.error(error) if LOGGER.level >= logging.INFO: LOGGER.error( - "Running again with option '-v' may give more information." - ) + "Running again with option '-v' may give more information." + ) sys.exit(1) except KeyboardInterrupt: LOGGER.warning("Aborted by user.") diff --git a/patacrep/songs/__init__.py b/patacrep/songs/__init__.py index 53da8d48..2afbb8b5 100644 --- a/patacrep/songs/__init__.py +++ b/patacrep/songs/__init__.py @@ -85,16 +85,16 @@ class Song(Content): # List of attributes to cache cached_attributes = [ - "titles", - "unprefixed_titles", - "cached", - "data", - "subpath", - "languages", - "authors", - "_filehash", - "_version", - ] + "titles", + "unprefixed_titles", + "cached", + "data", + "subpath", + "languages", + "authors", + "_filehash", + "_version", + ] def __init__(self, datadir, subpath, config): self.fullpath = os.path.join(datadir, subpath) @@ -105,8 +105,8 @@ class Song(Content): if datadir: # Only songs in datadirs are cached self._filehash = hashlib.md5( - open(self.fullpath, 'rb').read() - ).hexdigest() + open(self.fullpath, 'rb').read() + ).hexdigest() if os.path.exists(cached_name(datadir, subpath)): try: cached = pickle.load(open( @@ -116,7 +116,7 @@ class Song(Content): if ( cached['_filehash'] == self._filehash and cached['_version'] == self.CACHE_VERSION - ): + ): for attribute in self.cached_attributes: setattr(self, attribute, cached[attribute]) return @@ -135,17 +135,17 @@ class Song(Content): self.datadir = datadir self.subpath = subpath self.unprefixed_titles = [ - unprefixed_title( - title, - config['titleprefixwords'] - ) - for title - in self.titles - ] - self.authors = process_listauthors( - self.authors, - **config["_compiled_authwords"] + unprefixed_title( + title, + config['titleprefixwords'] ) + for title + in self.titles + ] + self.authors = process_listauthors( + self.authors, + **config["_compiled_authwords"] + ) # Cache management self._version = self.CACHE_VERSION @@ -158,10 +158,10 @@ class Song(Content): for attribute in self.cached_attributes: cached[attribute] = getattr(self, attribute) pickle.dump( - cached, - open(cached_name(self.datadir, self.subpath), 'wb'), - protocol=-1 - ) + cached, + open(cached_name(self.datadir, self.subpath), 'wb'), + protocol=-1 + ) def __repr__(self): return repr((self.titles, self.data, self.fullpath)) diff --git a/patacrep/songs/chordpro/__init__.py b/patacrep/songs/chordpro/__init__.py index 22e0b74b..cb0be42a 100644 --- a/patacrep/songs/chordpro/__init__.py +++ b/patacrep/songs/chordpro/__init__.py @@ -25,8 +25,8 @@ class ChordproSong(Song): self.languages = song.get_directives('language') self.data = dict([meta.as_tuple for meta in song.meta]) self.cached = { - 'song': song, - } + 'song': song, + } def tex(self, output): context = { @@ -40,8 +40,8 @@ class ChordproSong(Song): "render": self.render_tex, } self.texenv = Environment(loader=FileSystemLoader(os.path.join( - os.path.abspath(pkg_resources.resource_filename(__name__, 'data')), - 'latex' + os.path.abspath(pkg_resources.resource_filename(__name__, 'data')), + 'latex' ))) return self.render_tex(context, self.cached['song'].content, template="chordpro.tex") @@ -55,10 +55,10 @@ class ChordproSong(Song): if template is None: template = content.template('tex') return TexRenderer( - template=template, - encoding='utf8', - texenv=self.texenv, - ).template.render(context) + template=template, + encoding='utf8', + texenv=self.texenv, + ).template.render(context) SONG_PARSERS = { 'sgc': ChordproSong, diff --git a/patacrep/songs/chordpro/ast.py b/patacrep/songs/chordpro/ast.py index 47ffb8df..b82b46a9 100644 --- a/patacrep/songs/chordpro/ast.py +++ b/patacrep/songs/chordpro/ast.py @@ -146,9 +146,9 @@ class Verse(AST): def __str__(self): return '{{start_of_{type}}}\n{content}\n{{end_of_{type}}}'.format( - type=self.type, - content=_indent("\n".join([str(line) for line in self.lines])), - ) + type=self.type, + content=_indent("\n".join([str(line) for line in self.lines])), + ) class Chorus(Verse): """Chorus""" @@ -182,10 +182,10 @@ class Song(AST): #: Some directives have to be processed before being considered. PROCESS_DIRECTIVE = { - "cov": "_process_relative", - "partition": "_process_relative", - "image": "_process_relative", - } + "cov": "_process_relative", + "partition": "_process_relative", + "image": "_process_relative", + } def __init__(self, filename): super().__init__() @@ -242,12 +242,12 @@ class Song(AST): def __str__(self): return ( - "\n".join(self.str_meta()).strip() - + - "\n========\n" - + - "\n".join([str(item) for item in self.content]).strip() - ) + "\n".join(self.str_meta()).strip() + + + "\n========\n" + + + "\n".join([str(item) for item in self.content]).strip() + ) def add_title(self, __ignored, title): @@ -372,9 +372,9 @@ class Directive(AST): def __str__(self): if self.argument is not None: return "{{{}: {}}}".format( - self.keyword, - self.argument, - ) + self.keyword, + self.argument, + ) else: return "{{{}}}".format(self.keyword) @@ -405,6 +405,6 @@ class Tab(AST): def __str__(self): return '{{start_of_tab}}\n{}\n{{end_of_tab}}'.format( - _indent("\n".join(self.content)), - ) + _indent("\n".join(self.content)), + ) diff --git a/patacrep/songs/chordpro/lexer.py b/patacrep/songs/chordpro/lexer.py index 58792fa2..7f1582e6 100644 --- a/patacrep/songs/chordpro/lexer.py +++ b/patacrep/songs/chordpro/lexer.py @@ -39,7 +39,7 @@ class ChordProLexer: t_SPACE = r'[ \t]+' - t_chord_CHORD = r'[A-G7#m]+' # TODO This can be refined + t_chord_CHORD = r'[A-G7#m]+' t_directive_SPACE = r'[ \t]+' t_directive_KEYWORD = r'[a-zA-Z_]+' diff --git a/patacrep/songs/chordpro/syntax.py b/patacrep/songs/chordpro/syntax.py index 68afc738..fdcf9b72 100644 --- a/patacrep/songs/chordpro/syntax.py +++ b/patacrep/songs/chordpro/syntax.py @@ -1,54 +1,24 @@ -# -*- coding: utf-8 -*- """ChordPro parser""" import logging import ply.yacc as yacc -from patacrep.errors import SongbookError +from patacrep.songs.syntax import Parser from patacrep.songs.chordpro import ast from patacrep.songs.chordpro.lexer import tokens, ChordProLexer LOGGER = logging.getLogger() -class ParsingError(SongbookError): - """Parsing error.""" - - def __init__(self, message): - super().__init__(self) - self.message = message - - def __str__(self): - return self.message - - -class Parser: +class ChordproParser(Parser): """ChordPro parser class""" start = "song" def __init__(self, filename=None): + super().__init__() self.tokens = tokens self.filename = filename - @staticmethod - def __find_column(token): - """Return the column of ``token``.""" - last_cr = token.lexer.lexdata.rfind('\n', 0, token.lexpos) - if last_cr < 0: - last_cr = 0 - column = (token.lexpos - last_cr) + 1 - return column - - def p_error(self, token): - """Manage parsing errors.""" - if token: - LOGGER.error("Error in file {}, line {}:{}.".format( - str(self.filename), - token.lineno, - self.__find_column(token), - ) - ) - def p_song(self, symbols): """song : block song | empty @@ -208,10 +178,10 @@ class Parser: def parse_song(content, filename=None): """Parse song and return its metadata.""" return yacc.yacc( - module=Parser(filename), - debug=0, - write_tables=0, - ).parse( - content, - lexer=ChordProLexer().lexer, - ) + module=ChordproParser(filename), + debug=0, + write_tables=0, + ).parse( + content, + lexer=ChordProLexer().lexer, + ) diff --git a/patacrep/songs/chordpro/test/test_parser.py b/patacrep/songs/chordpro/test/test_parser.py index 2743522e..7f1c2586 100644 --- a/patacrep/songs/chordpro/test/test_parser.py +++ b/patacrep/songs/chordpro/test/test_parser.py @@ -49,8 +49,8 @@ def load_tests(__loader, tests, __pattern): """Load several tests given test files present in the directory.""" # Load all txt files as tests for txt in sorted(glob.glob(os.path.join( - os.path.dirname(__file__), - '*.txt', + os.path.dirname(__file__), + '*.txt', ))): tests.addTest(ParserTxtRenderer(basename=txt[:-len('.txt')])) return tests diff --git a/patacrep/songs/latex/__init__.py b/patacrep/songs/latex/__init__.py index ce08d368..8a0893c1 100644 --- a/patacrep/songs/latex/__init__.py +++ b/patacrep/songs/latex/__init__.py @@ -28,10 +28,10 @@ class LatexSong(Song): def tex(self, output): """Return the LaTeX code rendering the song.""" return r'\input{{{}}}'.format(files.path2posix( - files.relpath( - self.fullpath, - os.path.dirname(output) - ))) + files.relpath( + self.fullpath, + os.path.dirname(output) + ))) SONG_PARSERS = { 'is': LatexSong, diff --git a/patacrep/songs/syntax.py b/patacrep/songs/syntax.py new file mode 100644 index 00000000..aa2075a2 --- /dev/null +++ b/patacrep/songs/syntax.py @@ -0,0 +1,33 @@ +"""Generic parsing classes and methods""" + +import logging + +LOGGER = logging.getLogger() + +class Parser: + """Parser class""" + # pylint: disable=too-few-public-methods + + def __init__(self): + self.filename = "" # Will be overloaded + + @staticmethod + def __find_column(token): + """Return the column of ``token``.""" + last_cr = token.lexer.lexdata.rfind('\n', 0, token.lexpos) + if last_cr < 0: + last_cr = 0 + column = (token.lexpos - last_cr) + 1 + return column + + def p_error(self, token): + """Manage parsing errors.""" + if token: + LOGGER.error( + "Error in file {}, line {}:{}.".format( + str(self.filename), + token.lineno, + self.__find_column(token), + ) + ) + diff --git a/patacrep/templates.py b/patacrep/templates.py index e3f44847..95c06469 100644 --- a/patacrep/templates.py +++ b/patacrep/templates.py @@ -20,7 +20,8 @@ _LATEX_SUBS = ( (re.compile(r'\.\.\.+'), r'\\ldots'), ) -_VARIABLE_REGEXP = re.compile(r""" +_VARIABLE_REGEXP = re.compile( + r""" \(\*\ *variables\ *\*\) # Match (* variables *) ( # Match and capture the following: (?: # Start of non-capturing group, used to match a single character @@ -51,9 +52,9 @@ class VariablesExtension(Extension): def parse(self, parser): next(parser.stream) parser.parse_statements( - end_tokens=['name:endvariables'], - drop_needle=True, - ) + end_tokens=['name:endvariables'], + drop_needle=True, + ) return nodes.Const("") # pylint: disable=no-value-for-parameter @@ -101,29 +102,31 @@ class TexBookRenderer(TexRenderer): ''' self.lang = lang # Load templates in filesystem ... - loaders = [FileSystemLoader(os.path.join(datadir, 'templates')) - for datadir in datadirs] + loaders = [ + FileSystemLoader(os.path.join(datadir, 'templates')) + for datadir in datadirs + ] texenv = Environment( - loader=ChoiceLoader(loaders), - extensions=[VariablesExtension], - ) + loader=ChoiceLoader(loaders), + extensions=[VariablesExtension], + ) try: super().__init__(template, texenv, encoding) except TemplateNotFound as exception: # Only works if all loaders are FileSystemLoader(). paths = [ - item - for loader in self.texenv.loader.loaders - for item in loader.searchpath - ] + item + for loader in self.texenv.loader.loaders + for item in loader.searchpath + ] raise errors.TemplateError( - exception, - errors.notfound( - exception.name, - paths, - message='Template "{name}" not found in {paths}.' - ), - ) + exception, + errors.notfound( + exception.name, + paths, + message='Template "{name}" not found in {paths}.' + ), + ) def get_variables(self): '''Get and return a dictionary with the default values @@ -175,11 +178,11 @@ class TexBookRenderer(TexRenderer): if subtemplate in skip: continue variables.update( - self.get_template_variables( - subtemplate, - skip + templates - ) + self.get_template_variables( + subtemplate, + skip + templates ) + ) variables.update(current) return variables @@ -210,16 +213,16 @@ class TexBookRenderer(TexRenderer): subvariables.update(json.loads(var)) except ValueError as exception: raise errors.TemplateError( - exception, - ( - "Error while parsing json in file " - "{filename}. The json string was:" - "\n'''\n{jsonstring}\n'''" - ).format( - filename=templatename, - jsonstring=var, - ) + exception, + ( + "Error while parsing json in file " + "{filename}. The json string was:" + "\n'''\n{jsonstring}\n'''" + ).format( + filename=templatename, + jsonstring=var, ) + ) return (subvariables, subtemplates) diff --git a/pylintrc b/pylintrc new file mode 100644 index 00000000..d37fffc3 --- /dev/null +++ b/pylintrc @@ -0,0 +1,5 @@ +[VARIABLES] +dummy-variables-rgx=_|dummy + +[MESSAGES CONTROL] +disable= logging-format-interpolation