From d06638681220c6df3c0ba7be17eb089af40f44f0 Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 14 Jun 2014 21:56:53 +0200 Subject: [PATCH] The code is pylint compliant. --- songbook | 2 +- songbook_core/authors.py | 7 +- songbook_core/build.py | 24 ++++- songbook_core/content/__init__.py | 133 +++++++++++++++++++++++---- songbook_core/content/cwd.py | 20 ++++ songbook_core/content/section.py | 37 ++++++-- songbook_core/content/song.py | 41 ++++++++- songbook_core/content/songsection.py | 29 +++++- songbook_core/content/sorted.py | 39 +++++++- songbook_core/index.py | 5 +- songbook_core/plastex.py | 1 + songbook_core/songs.py | 6 +- songbook_core/templates.py | 4 +- 13 files changed, 303 insertions(+), 45 deletions(-) diff --git a/songbook b/songbook index 40c3958a..c108b21f 100755 --- a/songbook +++ b/songbook @@ -32,7 +32,7 @@ class ParseStepsAction(argparse.Action): ( getattr(namespace, self.dest) + [value.strip() for value in values[0].split(',')] - ), + ), ) class VerboseAction(argparse.Action): diff --git a/songbook_core/authors.py b/songbook_core/authors.py index 3f58b5af..32afb09a 100644 --- a/songbook_core/authors.py +++ b/songbook_core/authors.py @@ -12,8 +12,11 @@ DEFAULT_AUTHWORDS = { } def compile_authwords(authwords): - # Convert strings to regular expressions - # Fill holes + """Convert strings of authwords to compiled regular expressions. + + This regexp will later be used to match these words in authors strings. + """ + # Fill missing values for (key, value) in DEFAULT_AUTHWORDS.items(): if key not in authwords: authwords[key] = value diff --git a/songbook_core/build.py b/songbook_core/build.py index 6a2a01e6..213cb8cb 100755 --- a/songbook_core/build.py +++ b/songbook_core/build.py @@ -7,7 +7,6 @@ import codecs import glob import logging import os.path -import re from subprocess import Popen, PIPE, call from songbook_core import __DATADIR__ @@ -15,7 +14,6 @@ from songbook_core import authors from songbook_core import content from songbook_core import errors from songbook_core.index import process_sxd -from songbook_core.songs import Song from songbook_core.templates import TexRenderer LOGGER = logging.getLogger(__name__) @@ -54,6 +52,7 @@ class Songbook(object): super(Songbook, self).__init__() self.config = raw_songbook self.basename = basename + self.contentlist = [] # Some special keys have their value processed. self._set_datadir() @@ -70,14 +69,26 @@ class Songbook(object): if os.path.exists(path) and os.path.isdir(path): abs_datadir.append(os.path.abspath(path)) else: - LOGGER.warning("Ignoring non-existent datadir '{}'.".format(path)) + LOGGER.warning( + "Ignoring non-existent datadir '{}'.".format(path) + ) abs_datadir.append(__DATADIR__) self.config['datadir'] = abs_datadir - self.config['_songdir'] = [os.path.join(path, 'songs') for path in self.config['datadir']] + self.config['_songdir'] = [ + os.path.join(path, 'songs') + for path in self.config['datadir'] + ] def build_config(self, from_templates): + """Build configuration dictionary + + This dictionary is assembled using (by order of least precedence): + - the hard-coded default; + - the values read from templates; + - the values read from .sb file. + """ config = DEFAULT_CONFIG config.update(from_templates) config.update(self.config) @@ -100,7 +111,10 @@ class Songbook(object): ) context = self.build_config(renderer.get_variables()) - self.contentlist = content.process_content(self.config.get('content', []), context) + self.contentlist = content.process_content( + self.config.get('content', []), + context, + ) context['render_content'] = content.render_content context['titleprefixkeys'] = ["after", "sep", "ignore"] context['content'] = self.contentlist diff --git a/songbook_core/content/__init__.py b/songbook_core/content/__init__.py index 2c3498a6..18757f73 100644 --- a/songbook_core/content/__init__.py +++ b/songbook_core/content/__init__.py @@ -1,6 +1,72 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +"""Content plugin management. + +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. + +# Plugin definition + +A plugin is a submodule of this module, which have a variable +CONTENT_PLUGINS, which is a dictionary where: + - keys are keywords, + - values are parsers (see below). + +When analysing the content field of the .sb file, when those keywords are +met, the corresponding parser is called. + +# Parsers + +A parser is a function which takes as arguments: + - keyword: the keyword triggering this function; + - argument: the argument of the keyword (see below); + - contentlist: the list of content, that is, the part of the list + following the keyword (see example below); + - 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). + +Example: When the following piece of content is met + + ["sorted(author, @title)", "a_song.sg", "another_song.sg"] + +the parser associated to keyword 'sorted' get the arguments: + - keyword = "sorted" + - argument = "author, @title" + - contentlist = ["a_song.sg", "another_song.sg"] + - config = . + +# Keyword + +A keyword is either an identifier (alphanumeric characters, and underscore), +or such an identifier, with some text surrounded by parenthesis (like a +function definition); this text is called the argument to the keyword. +Examples: + - sorted + - sorted(author, @title) + - cwd(some/path) + +If the keyword has an argument, it can be anything, given that it is +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 + +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 +anything in the resulting .tex. + +More documentation in the docstring of Content. + +""" + import glob import importlib import jinja2 @@ -13,10 +79,15 @@ from songbook_core.errors import SongbookError LOGGER = logging.getLogger(__name__) EOL = '\n' -class Content: - """Content item of type 'example'.""" +#pylint: disable=no-self-use +class Content(object): + """Content item. Will render to something in the .tex file. + + The current jinja2.runtime.Context is passed to all function defined + here. + """ - def render(self, context): + def render(self, __context): """Render this content item. Returns a string, to be placed verbatim in the generated .tex file. @@ -25,31 +96,35 @@ class Content: # Block management - def begin_new_block(self, previous, context): + def begin_new_block(self, __previous, __context): """Return a boolean stating if a new block is to be created. # Arguments - - previous: the songbook.content.Content object of the previous item. - - context: current jinja2.runtime.Context. + - __previous: the songbook.content.Content object of the previous item. + - __context: see Content() documentation. # Return - True if the renderer has to close previous block, and begin a new one, - False otherwise. + - True if the renderer has to close previous block, and begin a new + one, + - False otherwise (the generated code for this item is part of the + current block). """ return True - def begin_block(self, context): + def begin_block(self, __context): """Return the string to begin a block.""" return "" - def end_block(self, context): + def end_block(self, __context): """Return the string to end a block.""" return "" class ContentError(SongbookError): + """Error in a content plugin.""" def __init__(self, keyword, message): + super(ContentError, self).__init__() self.keyword = keyword self.message = message @@ -67,17 +142,31 @@ def load_plugins(): for name in glob.glob(os.path.join(os.path.dirname(__file__), "*.py")): if name.endswith(".py") and os.path.basename(name) != "__init__.py": plugin = importlib.import_module( - 'songbook_core.content.{}'.format(os.path.basename(name[:-len('.py')])) - ) + 'songbook_core.content.{}'.format( + os.path.basename(name[:-len('.py')]) + ) + ) for (key, value) in plugin.CONTENT_PLUGINS.items(): if key in plugins: - LOGGER.warning("File %s: Keyword '%s' is already used. Ignored.", os.path.relpath(name), key) + LOGGER.warning( + "File %s: Keyword '%s' is already used. Ignored.", + os.path.relpath(name), + key, + ) continue plugins[key] = value return plugins @jinja2.contextfunction def render_content(context, content): + """Render the content of the songbook as a LaTeX code. + + Arguments: + - context: the jinja2.runtime.context of the current template + compilation. + - content: a list of Content() instances, as the one that was returned by + process_content(). + """ rendered = "" previous = None last = None @@ -99,7 +188,17 @@ def render_content(context, content): return rendered -def process_content(content, config = None): +def process_content(content, config=None): + """Process content, and return a list of Content() 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 + included in the .tex file. + """ contentlist = [] plugins = load_plugins() keyword_re = re.compile(r'^ *(?P\w*) *(\((?P.*)\))? *$') @@ -119,8 +218,8 @@ def process_content(content, config = None): raise ContentError(keyword, "Unknown content type.") contentlist.extend(plugins[keyword]( keyword, - argument = argument, - contentlist = elem[1:], - config = config, + argument=argument, + contentlist=elem[1:], + config=config, )) return contentlist diff --git a/songbook_core/content/cwd.py b/songbook_core/content/cwd.py index a9c5053e..e4a71992 100644 --- a/songbook_core/content/cwd.py +++ b/songbook_core/content/cwd.py @@ -1,11 +1,31 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +"""Change base directory before importing songs.""" + import os from songbook_core.content import process_content +#pylint: disable=unused-argument def parse(keyword, config, argument, contentlist): + """Return a list songs included in contentlist, whith a different base path. + + Arguments: + - keyword: unused; + - config: the current songbook configuration dictionary; + - argument: a directory; + - contentlist: songbook content, that is parsed by + songbook_core.content.process_content(). + + This function adds 'argument' to the directories where songs are searched + for, and then processes the content. + + The 'argument' is added: + - first as a relative path to the current directory; + - then as a relative path to every path already present in + config['songdir']. + """ config['_songdir'] = ( [os.path.relpath(argument)] + [os.path.join(path, argument) for path in config['_songdir']] + diff --git a/songbook_core/content/section.py b/songbook_core/content/section.py index 74f9e31e..7ce247bf 100644 --- a/songbook_core/content/section.py +++ b/songbook_core/content/section.py @@ -1,7 +1,9 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from songbook_core.content import Content +"""Allow LaTeX sections (starred or not) as content of a songbook.""" + +from songbook_core.content import Content, ContentError KEYWORDS = [ "part", @@ -12,26 +14,47 @@ KEYWORDS = [ "paragraph", "subparagraph", ] -FULL_KEYWORDS = KEYWORDS + [ "{}*".format(keyword) for keyword in KEYWORDS] +FULL_KEYWORDS = KEYWORDS + ["{}*".format(word) for word in KEYWORDS] class Section(Content): - def __init__(self, keyword, name, short = None): + """A LaTeX section.""" + + def __init__(self, keyword, name, short=None): self.keyword = keyword self.name = name self.short = short def render(self, __context): - if (self.short is None): + if self.short is None: return r'\{}{{{}}}'.format(self.keyword, self.name) else: return r'\{}[{}]{{{}}}'.format(self.keyword, self.short, self.name) +#pylint: disable=unused-argument def parse(keyword, argument, contentlist, config): + """Parse the contentlist. + + Arguments: + - keyword (one of "part", "chapter", "section", ... , "subparagraph", and + their starred versions "part*", "chapter*", ... , "subparagraph*"): the + section to use; + - argument: unused; + - contentlist: a list of one or two strings, which are the names (short + and long) of the section; + - config: configuration dictionary of the current songbook. + """ if (keyword not in KEYWORDS) and (len(contentlist) != 1): - raise ContentError(keyword, "Starred section names must have exactly one argument.") + raise ContentError( + 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)] + return [Section(keyword, *contentlist)] #pylint: disable=star-args -CONTENT_PLUGINS = dict([(keyword, parse) for keyword in FULL_KEYWORDS]) +CONTENT_PLUGINS = dict([ + (word, parse) + for word + in FULL_KEYWORDS + ]) diff --git a/songbook_core/content/song.py b/songbook_core/content/song.py index fbaae8d7..885aba6d 100644 --- a/songbook_core/content/song.py +++ b/songbook_core/content/song.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +"""Plugin to include songs to the songbook.""" + import glob import jinja2 import logging @@ -13,19 +15,25 @@ from songbook_core.songs import Song LOGGER = logging.getLogger(__name__) class SongRenderer(Content, Song): + """Render a song in the .tex file.""" + def begin_new_block(self, previous, __context): + """Return a boolean stating if a new block is to be created.""" return not isinstance(previous, SongRenderer) def begin_block(self, context): + """Return the string to begin a block.""" indexes = context.resolve("indexes") if isinstance(indexes, jinja2.runtime.Undefined): indexes = "" return r'\begin{songs}{%s}' % indexes def end_block(self, __context): + """Return the string to end a block.""" return r'\end{songs}' def render(self, context): + """Return the string that will render the song.""" outdir = os.path.dirname(context['filename']) if os.path.abspath(self.path).startswith(os.path.abspath(outdir)): path = os.path.relpath(self.path, outdir) @@ -33,7 +41,19 @@ class SongRenderer(Content, Song): path = os.path.abspath(self.path) return r'\input{{{}}}'.format(path) +#pylint: disable=unused-argument def parse(keyword, argument, contentlist, config): + """Parse data associated with keyword 'song'. + + Arguments: + - keyword: unused; + - argument: unused; + - contentlist: a list of strings, which are interpreted as regular + expressions (interpreted using the glob module), referring to songs. + - config: the current songbook configuration dictionary. + + Return a list of SongRenderer() instances. + """ if 'languages' not in config: config['_languages'] = set() songlist = [] @@ -67,15 +87,30 @@ CONTENT_PLUGINS = {'song': parse} class OnlySongsError(ContentError): + "A list that should contain only songs also contain other type of content." def __init__(self, not_songs): + super(OnlySongsError, self).__init__() self.not_songs = not_songs def __str__(self): - return "Only songs are allowed, and the following items are not:" + str(not_songs) + return ( + "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. -def process_songs(content, config = None): + Call songbook_core.content.process_content(), checks if the returned list + contains only songs, and raise an exception if not. + """ contentlist = process_content(content, config) - not_songs = [item for item in contentlist if not isinstance(item, SongRenderer)] + not_songs = [ + item + for item + in contentlist + if not isinstance(item, SongRenderer) + ] if not_songs: raise OnlySongsError(not_songs) return contentlist diff --git a/songbook_core/content/songsection.py b/songbook_core/content/songsection.py index 1d1abd14..32ff40af 100644 --- a/songbook_core/content/songsection.py +++ b/songbook_core/content/songsection.py @@ -1,7 +1,9 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from songbook_core.content import Content +"""Allow 'songchapter' and 'songsection' as content of a songbook.""" + +from songbook_core.content import Content, ContentError KEYWORDS = [ "songchapter", @@ -9,17 +11,36 @@ KEYWORDS = [ ] class SongSection(Content): + """A songsection or songchapter.""" + def __init__(self, keyword, name): self.keyword = keyword self.name = name def render(self, __context): + """Render this section or chapter.""" return r'\{}{{{}}}'.format(self.keyword, self.name) +#pylint: disable=unused-argument def parse(keyword, argument, contentlist, config): + """Parse the contentlist. + + Arguments: + - keyword ("songsection" or "songchapter"): the section to use; + - argument: unused; + - contentlist: a list of one string, which is the name of the section; + - config: configuration dictionary of the current songbook. + """ if (keyword not in KEYWORDS) and (len(contentlist) != 1): - raise ContentError(keyword, "Starred section names must have exactly one argument.") - return [SongSection(keyword, *contentlist)] + raise ContentError( + keyword, + "Starred section names must have exactly one argument.", + ) + return [SongSection(keyword, contentlist[0])] -CONTENT_PLUGINS = dict([(keyword, parse) for keyword in KEYWORDS]) +CONTENT_PLUGINS = dict([ + (word, parse) + for word + in KEYWORDS + ]) diff --git a/songbook_core/content/sorted.py b/songbook_core/content/sorted.py index d4ae45df..030c6e12 100644 --- a/songbook_core/content/sorted.py +++ b/songbook_core/content/sorted.py @@ -1,23 +1,45 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +"""Sorted list of songs. + +This plugin provides keyword 'sorted', used to include a sorted list of songs +to a songbook. +""" + import locale +from songbook_core.content import ContentError from songbook_core.content.song import OnlySongsError, process_songs DEFAULT_SORT = ['by', 'album', '@title'] def normalize_string(string): + """Return a normalized string. + + Normalized means: + - no surrounding spaces; + - lower case; + - passed through locale.strxfrm(). + """ return locale.strxfrm(string.lower().strip()) def normalize_field(field): + """Return a normalized field, it being a string or a list of strings.""" if isinstance(field, basestring): return normalize_string(field) elif isinstance(field, list): return [normalize_string(string) for string in field] def key_generator(sort): + """Return a function that returns the list of values used to sort the song. + + Arguments: + - sort: the list of keys used to sort. + """ + def ordered_song_keys(song): + """Return the list of values used to sort the song.""" songkey = [] for key in sort: if key == "@title": @@ -32,7 +54,18 @@ def key_generator(sort): return songkey return ordered_song_keys +#pylint: disable=unused-argument def parse(keyword, config, argument, contentlist): + """Return a sorted list of songs contained in 'contentlist'. + + Arguments: + - keyword: the string 'sorted'; + - config: the current songbook configuration dictionary; + - argument: the list of the fields used to sort songs, as strings + separated by commas (e.g. "by, album, @title"); + - contentlist: the list of content to be sorted. If this content + contain something else than a song, an exception is raised. + """ if argument: sort = [key.strip() for key in argument.split(",")] else: @@ -40,7 +73,11 @@ def parse(keyword, config, argument, contentlist): try: songlist = process_songs(contentlist, config) except OnlySongsError as error: - raise ContentError(keyword, "Content list of this keyword can bo only songs (or content that result into songs), and the following are not:" + str(error.not_songs)) + raise ContentError(keyword, ( + "Content list of this keyword can bo only songs (or content " + "that result into songs), and the following are not:" + + str(error.not_songs) + )) return sorted(songlist, key=key_generator(sort)) CONTENT_PLUGINS = {'sorted': parse} diff --git a/songbook_core/index.py b/songbook_core/index.py index 67c1d410..50c5f946 100755 --- a/songbook_core/index.py +++ b/songbook_core/index.py @@ -30,7 +30,9 @@ def sortkey(value): don't forget to call locale.setlocale(locale.LC_ALL, '')). It also handles the sort with latex escape sequences. """ - return locale.strxfrm(unidecode(simpleparse(value).replace(' ', 'A')).lower()) + return locale.strxfrm( + unidecode(simpleparse(value).replace(' ', 'A')).lower() + ) def process_sxd(filename): @@ -65,6 +67,7 @@ class Index(object): def __init__(self, indextype): self.data = dict() self.keywords = dict() + self.authwords = dict() self.prefix_patterns = [] if indextype == "TITLE INDEX DATA FILE": self.indextype = "TITLE" diff --git a/songbook_core/plastex.py b/songbook_core/plastex.py index 7f5980ea..45bd0258 100644 --- a/songbook_core/plastex.py +++ b/songbook_core/plastex.py @@ -13,6 +13,7 @@ import sys def process_unbr_spaces(node): + #pylint: disable=line-too-long r"""Replace '~' and '\ ' in node by nodes that will be rendered as unbreakable space. diff --git a/songbook_core/songs.py b/songbook_core/songs.py index f959466a..51f56b52 100644 --- a/songbook_core/songs.py +++ b/songbook_core/songs.py @@ -4,7 +4,6 @@ """Song management.""" from unidecode import unidecode -import locale import re from songbook_core.authors import processauthors @@ -30,7 +29,10 @@ class Song(object): self.path = filename self.languages = data['languages'] if "by" in self.args.keys(): - self.authors = processauthors(self.args["by"], **config["authwords"]) + self.authors = processauthors( + self.args["by"], + **config["authwords"] + ) else: self.authors = [] diff --git a/songbook_core/templates.py b/songbook_core/templates.py index 05f2ba3d..18acb64c 100644 --- a/songbook_core/templates.py +++ b/songbook_core/templates.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- """Template for .tex generation settings and utilities""" -from jinja2 import Environment, FileSystemLoader, ChoiceLoader, PackageLoader, \ +from jinja2 import Environment, FileSystemLoader, ChoiceLoader, \ TemplateNotFound, nodes from jinja2.ext import Extension from jinja2.meta import find_referenced_templates as find_templates @@ -54,7 +54,7 @@ class VariablesExtension(Extension): end_tokens=['name:endvariables'], drop_needle=True, ) - return nodes.Const("") + return nodes.Const("") # pylint: disable=no-value-for-parameter def _escape_tex(value):