Browse Source

The code is pylint compliant.

pull/47/head
Louis 11 years ago
parent
commit
d066386812
  1. 7
      songbook_core/authors.py
  2. 24
      songbook_core/build.py
  3. 123
      songbook_core/content/__init__.py
  4. 20
      songbook_core/content/cwd.py
  5. 35
      songbook_core/content/section.py
  6. 39
      songbook_core/content/song.py
  7. 29
      songbook_core/content/songsection.py
  8. 39
      songbook_core/content/sorted.py
  9. 5
      songbook_core/index.py
  10. 1
      songbook_core/plastex.py
  11. 6
      songbook_core/songs.py
  12. 4
      songbook_core/templates.py

7
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

24
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

123
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 = <the config file of the current songbook>.
# 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
@ -100,6 +189,16 @@ def render_content(context, content):
return rendered
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<keyword>\w*) *(\((?P<argument>.*)\))? *$')

20
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']] +

35
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):
"""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
])

39
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.
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

29
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
])

39
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}

5
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"

1
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.

6
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 = []

4
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):

Loading…
Cancel
Save