Browse Source

Merge branch 'next' of https://github.com/patacrep/songbook-core into next

pull/27/head
Luthaf 11 years ago
parent
commit
1c7435e40d
  1. 35
      songbook
  2. 262
      songbook_core/build.py
  3. 2
      songbook_core/errors.py
  4. 2
      songbook_core/plastex.py
  5. 9
      songbook_core/songs.py

35
songbook

@ -12,10 +12,13 @@ import os.path
import textwrap import textwrap
import sys import sys
from songbook_core.build import buildsongbook, DEFAULT_STEPS from songbook_core.build import SongbookBuilder, DEFAULT_STEPS
from songbook_core import __STR_VERSION__ from songbook_core import __STR_VERSION__
from songbook_core import errors from songbook_core import errors
# Logging configuration
logging.basicConfig(level=logging.INFO)
LOGGER = logging.getLogger()
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
class ParseStepsAction(argparse.Action): class ParseStepsAction(argparse.Action):
@ -32,6 +35,11 @@ class ParseStepsAction(argparse.Action):
), ),
) )
class VerboseAction(argparse.Action):
"""Set verbosity level with option --verbose."""
def __call__(self, *_args, **_kwargs):
LOGGER.setLevel(logging.DEBUG)
def argument_parser(args): def argument_parser(args):
"""Parse argumnts""" """Parse argumnts"""
parser = argparse.ArgumentParser(description="A song book compiler") parser = argparse.ArgumentParser(description="A song book compiler")
@ -49,6 +57,11 @@ def argument_parser(args):
subdirectories are 'songs', 'img', 'latex', 'templates'. 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, parser.add_argument('--steps', '-s', nargs=1, type=str,
action=ParseStepsAction, action=ParseStepsAction,
help=textwrap.dedent("""\ help=textwrap.dedent("""\
@ -74,10 +87,6 @@ def argument_parser(args):
def main(): def main():
"""Main function:""" """Main function:"""
# Logging configuration
logging.basicConfig(name='songbook')
logger = logging.getLogger('songbook')
# set script locale to match user's # set script locale to match user's
try: try:
locale.setlocale(locale.LC_ALL, '') locale.setlocale(locale.LC_ALL, '')
@ -105,15 +114,15 @@ def main():
songbook['datadir'] = os.path.dirname(songbook_path) songbook['datadir'] = os.path.dirname(songbook_path)
try: try:
buildsongbook( sb_builder = SongbookBuilder(songbook, basename)
songbook, sb_builder.unsafe = True
basename,
steps=options.steps, sb_builder.build_steps(options.steps)
interactive=True,
logger=logger,
)
except errors.SongbookError as error: except errors.SongbookError as error:
logger.error(error) LOGGER.error(error)
LOGGER.error(
"Running again with option '-v' may give more information."
)
sys.exit(1) sys.exit(1)
sys.exit(0) sys.exit(0)

262
songbook_core/build.py

@ -8,7 +8,7 @@ import glob
import logging import logging
import os.path import os.path
import re import re
import subprocess from subprocess import Popen, PIPE, call
from songbook_core import __DATADIR__ from songbook_core import __DATADIR__
from songbook_core import errors from songbook_core import errors
@ -17,6 +17,7 @@ from songbook_core.index import process_sxd
from songbook_core.songs import Song, SongsList from songbook_core.songs import Song, SongsList
from songbook_core.templates import TexRenderer from songbook_core.templates import TexRenderer
LOGGER = logging.getLogger(__name__)
EOL = "\n" EOL = "\n"
DEFAULT_AUTHWORDS = { DEFAULT_AUTHWORDS = {
"after": ["by"], "after": ["by"],
@ -24,6 +25,18 @@ DEFAULT_AUTHWORDS = {
"sep": ["and"], "sep": ["and"],
} }
DEFAULT_STEPS = ['tex', 'pdf', 'sbx', 'pdf', 'clean'] DEFAULT_STEPS = ['tex', 'pdf', 'sbx', 'pdf', 'clean']
GENERATED_EXTENSIONS = [
"_auth.sbx",
"_auth.sxd",
".aux",
".log",
".out",
".sxc",
".tex",
"_title.sbx",
"_title.sxd",
]
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
@ -47,9 +60,10 @@ class Songbook(object):
'datadir': os.path.abspath('.'), 'datadir': os.path.abspath('.'),
} }
self.songslist = None self.songslist = None
self._parse(raw_songbook) self._parse_raw(raw_songbook)
def _set_songs_default(self, config): @staticmethod
def _set_songs_default(config):
"""Set the default values for the Song() class. """Set the default values for the Song() class.
Argument: Argument:
@ -70,7 +84,7 @@ class Songbook(object):
] + [',']) ] + [','])
] ]
def _parse(self, raw_songbook): def _parse_raw(self, raw_songbook):
"""Parse raw_songbook. """Parse raw_songbook.
The principle is: some special keys have their value processed; others The principle is: some special keys have their value processed; others
@ -78,7 +92,7 @@ class Songbook(object):
""" """
self.config.update(raw_songbook) self.config.update(raw_songbook)
self.config['datadir'] = os.path.abspath(self.config['datadir']) self.config['datadir'] = os.path.abspath(self.config['datadir'])
### Some post-processing
# Compute song list # Compute song list
if self.config['content'] is None: if self.config['content'] is None:
self.config['content'] = [ self.config['content'] = [
@ -92,20 +106,24 @@ class Songbook(object):
'*.sg', '*.sg',
) )
] ]
self.songslist = SongsList(self.config['datadir'])
self.songslist.append_list(self.config['content'])
# Ensure self.config['authwords'] contains all entries # Ensure self.config['authwords'] contains all entries
for (key, value) in DEFAULT_AUTHWORDS.items(): for (key, value) in DEFAULT_AUTHWORDS.items():
if key not in self.config['authwords']: if key not in self.config['authwords']:
self.config['authwords'][key] = value self.config['authwords'][key] = value
def _parse_songs(self):
"""Parse songs included in songbook."""
self.songslist = SongsList(self.config['datadir'])
self.songslist.append_list(self.config['content'])
def write_tex(self, output): def write_tex(self, output):
"""Build the '.tex' file corresponding to self. """Build the '.tex' file corresponding to self.
Arguments: Arguments:
- output: a file object, in which the file will be written. - output: a file object, in which the file will be written.
""" """
self._parse_songs()
renderer = TexRenderer( renderer = TexRenderer(
self.config['template'], self.config['template'],
self.config['datadir'], self.config['datadir'],
@ -123,102 +141,138 @@ class Songbook(object):
renderer.render_tex(output, context) renderer.render_tex(output, context)
def clean(basename): class SongbookBuilder(object):
"""Clean (some) temporary files used during compilation. """Provide methods to compile a songbook."""
Depending of the LaTeX modules used in the template, there may be others
that are not deleted by this function."""
generated_extensions = [
"_auth.sbx",
"_auth.sxd",
".aux",
".log",
".out",
".sxc",
".tex",
"_title.sbx",
"_title.sxd",
]
for ext in generated_extensions:
if os.path.isfile(basename + ext):
try:
os.unlink(basename + ext)
except Exception as exception:
raise errors.CleaningError(basename + ext, exception)
def buildsongbook(
raw_songbook,
basename,
steps=None,
interactive=False,
logger=logging.getLogger(),
):
"""Build a songbook
Arguments:
- raw_songbook: Python representation of the .sb songbook configuration
file.
- steps: list of steps to perform to compile songbook. Available steps are:
- tex: build .tex file from templates;
- pdf: compile .tex using pdflatex;
- sbx: compile song and author indexes;
- clean: remove temporary files,
- any string beginning with a sharp sign (#): it is interpreted as a
command to run in a shell.
- basename: basename of the songbook to be built.
- interactive: in False, do not expect anything from stdin.
"""
if steps is None: # if False, do not expect anything from stdin.
steps = DEFAULT_STEPS interactive = False
# if True, allow unsafe option, like adding the --shell-escape to pdflatex
songbook = Songbook(raw_songbook, basename) unsafe = False
if not 'TEXINPUTS' in os.environ.keys(): # Options to add to pdflatex
os.environ['TEXINPUTS'] = '' _pdflatex_options = []
os.environ['TEXINPUTS'] += os.pathsep + os.path.join( # Dictionary of functions that have been called by self._run_once(). Keys
__DATADIR__, # are function; values are return values of functions.
'latex', _called_functions = {}
)
os.environ['TEXINPUTS'] += os.pathsep + os.path.join( def __init__(self, raw_songbook, basename):
songbook.config['datadir'], # Representation of the .sb songbook configuration file.
'latex', self.songbook = Songbook(raw_songbook, basename)
) # Basename of the songbook to be built.
self.basename = basename
# pdflatex options
pdflatex_options = [] def _run_once(self, function, *args, **kwargs):
pdflatex_options.append("--shell-escape") # Lilypond compilation """Run function if it has not been run yet.
if not interactive:
pdflatex_options.append("-halt-on-error") If it as, return the previous return value.
"""
# Compilation if function not in self._called_functions:
for step in steps: self._called_functions[function] = function(*args, **kwargs)
if step == 'tex': return self._called_functions[function]
# Building .tex file from templates
with codecs.open("{}.tex".format(basename), 'w', 'utf-8') as output: def _set_latex(self):
songbook.write_tex(output) """Set TEXINPUTS and LaTeX options."""
elif step == 'pdf': if not 'TEXINPUTS' in os.environ.keys():
if subprocess.call(["pdflatex"] + pdflatex_options + [basename]): os.environ['TEXINPUTS'] = ''
raise errors.LatexCompilationError(basename) os.environ['TEXINPUTS'] += os.pathsep + os.path.join(
elif step == 'sbx': __DATADIR__,
# Make index 'latex',
sxd_files = glob.glob("%s_*.sxd" % basename) )
for sxd_file in sxd_files: os.environ['TEXINPUTS'] += os.pathsep + os.path.join(
logger.info("processing " + sxd_file) self.songbook.config['datadir'],
idx = process_sxd(sxd_file) 'latex',
index_file = open(sxd_file[:-3] + "sbx", "w") )
if self.unsafe:
self._pdflatex_options.append("--shell-escape")
if not self.interactive:
self._pdflatex_options.append("-halt-on-error")
def build_steps(self, steps=None):
"""Perform steps on the songbook by calling relevant self.build_*()
Arguments:
- steps: list of steps to perform to compile songbook. Available steps
are:
- tex: build .tex file from templates;
- pdf: compile .tex using pdflatex;
- sbx: compile song and author indexes;
- clean: remove temporary files,
- any string beginning with a sharp sign (#): it is interpreted as a
command to run in a shell.
"""
if not steps:
steps = DEFAULT_STEPS
for step in steps:
if step == 'tex':
self.build_tex()
elif step == 'pdf':
self.build_pdf()
elif step == 'sbx':
self.build_sbx()
elif step == 'clean':
self.clean()
elif step.startswith("#"):
self.build_custom(step[1:])
else:
# Unknown step name
raise errors.UnknownStep(step)
def build_tex(self):
"""Build .tex file from templates"""
LOGGER.info("Building '{}.tex'".format(self.basename))
with codecs.open(
"{}.tex".format(self.basename), 'w', 'utf-8',
) as output:
self.songbook.write_tex(output)
def build_pdf(self):
"""Build .pdf file from .tex file"""
LOGGER.info("Building '{}.pdf'".format(self.basename))
self._run_once(self._set_latex)
process = Popen(
["pdflatex"] + self._pdflatex_options + [self.basename],
stdout=PIPE,
stderr=PIPE)
log = ''
line = process.stdout.readline()
while line:
log += line
line = process.stdout.readline()
LOGGER.debug(log)
process.wait()
if process.returncode:
raise errors.LatexCompilationError(self.basename)
def build_sbx(self):
"""Make index"""
LOGGER.info("Building indexes…")
sxd_files = glob.glob("%s_*.sxd" % self.basename)
for sxd_file in sxd_files:
LOGGER.debug("Processing " + sxd_file)
idx = process_sxd(sxd_file)
with open(sxd_file[:-3] + "sbx", "w") as index_file:
index_file.write(idx.entries_to_str().encode('utf8')) index_file.write(idx.entries_to_str().encode('utf8'))
index_file.close()
elif step == 'clean': @staticmethod
# Cleaning def build_custom(command):
clean(basename) """Run a shell command"""
elif step.startswith("%"): LOGGER.info("Running '{}'".format(command))
# Shell command exit_code = call(command, shell=True)
command = step[1:] if exit_code:
exit_code = subprocess.call(command, shell=True) raise errors.StepCommandError(command, exit_code)
if exit_code:
raise errors.StepCommandError(command, exit_code) def clean(self):
else: """Clean (some) temporary files used during compilation.
# Unknown step name
raise errors.UnknownStep(step) Depending of the LaTeX modules used in the template, there may be others
that are not deleted by this function."""
LOGGER.info("Cleaning…")
for ext in GENERATED_EXTENSIONS:
if os.path.isfile(self.basename + ext):
try:
os.unlink(self.basename + ext)
except Exception as exception:
raise errors.CleaningError(self.basename + ext, exception)

2
songbook_core/errors.py

@ -13,7 +13,7 @@ class SongbookError(Exception):
class TemplateError(SongbookError): class TemplateError(SongbookError):
"""Error during template generation""" """Error during template generation"""
def __init__(self, original, message = None): def __init__(self, original, message=None):
super(TemplateError, self).__init__() super(TemplateError, self).__init__()
self.original = original self.original = original
self.message = message self.message = message

2
songbook_core/plastex.py

@ -65,7 +65,7 @@ class SongParser(object):
def parse(cls, filename): def parse(cls, filename):
"""Parse a TeX file, and return its plasTeX representation.""" """Parse a TeX file, and return its plasTeX representation."""
tex = cls.create_tex() tex = cls.create_tex()
tex.input(codecs.open(filename, 'r+', 'utf-8', 'replace')) tex.input(codecs.open(filename, 'r', 'utf-8', 'replace'))
return tex.parse() return tex.parse()

9
songbook_core/songs.py

@ -8,10 +8,12 @@ import glob
import locale import locale
import os.path import os.path
import re import re
import logging
from songbook_core.authors import processauthors from songbook_core.authors import processauthors
from songbook_core.plastex import parsetex from songbook_core.plastex import parsetex
LOGGER = logging.getLogger(__name__)
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
class Song(object): class Song(object):
@ -101,6 +103,7 @@ class SongsList(object):
pour en extraire et traiter certaines information (titre, langue, pour en extraire et traiter certaines information (titre, langue,
album, etc.). album, etc.).
""" """
LOGGER.debug('Parsing file "{}"'.format(filename))
# Exécution de PlasTeX # Exécution de PlasTeX
data = parsetex(filename) data = parsetex(filename)
@ -122,8 +125,14 @@ class SongsList(object):
le module glob. le module glob.
""" """
for regexp in filelist: for regexp in filelist:
before = len(self.songs)
for filename in glob.iglob(os.path.join(self._songdir, regexp)): for filename in glob.iglob(os.path.join(self._songdir, regexp)):
self.append(filename) self.append(filename)
if len(self.songs) == before:
# No songs were added
LOGGER.warning(
"Expression '{}' did not match any file".format(regexp)
)
def languages(self): def languages(self):
"""Renvoie la liste des langues utilisées par les chansons""" """Renvoie la liste des langues utilisées par les chansons"""

Loading…
Cancel
Save