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. 214
      songbook_core/build.py
  3. 2
      songbook_core/plastex.py
  4. 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)

214
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,58 +141,36 @@ 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 False, do not expect anything from stdin.
if os.path.isfile(basename + ext): interactive = False
try: # if True, allow unsafe option, like adding the --shell-escape to pdflatex
os.unlink(basename + ext) unsafe = False
except Exception as exception: # Options to add to pdflatex
raise errors.CleaningError(basename + ext, exception) _pdflatex_options = []
# Dictionary of functions that have been called by self._run_once(). Keys
# are function; values are return values of functions.
_called_functions = {}
def __init__(self, raw_songbook, basename):
# Representation of the .sb songbook configuration file.
self.songbook = Songbook(raw_songbook, basename)
# Basename of the songbook to be built.
self.basename = basename
def buildsongbook( def _run_once(self, function, *args, **kwargs):
raw_songbook, """Run function if it has not been run yet.
basename,
steps=None,
interactive=False,
logger=logging.getLogger(),
):
"""Build a songbook
Arguments: If it as, return the previous return value.
- 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 function not in self._called_functions:
self._called_functions[function] = function(*args, **kwargs)
return self._called_functions[function]
if steps is None: def _set_latex(self):
steps = DEFAULT_STEPS """Set TEXINPUTS and LaTeX options."""
songbook = Songbook(raw_songbook, basename)
if not 'TEXINPUTS' in os.environ.keys(): if not 'TEXINPUTS' in os.environ.keys():
os.environ['TEXINPUTS'] = '' os.environ['TEXINPUTS'] = ''
os.environ['TEXINPUTS'] += os.pathsep + os.path.join( os.environ['TEXINPUTS'] += os.pathsep + os.path.join(
@ -182,43 +178,101 @@ def buildsongbook(
'latex', 'latex',
) )
os.environ['TEXINPUTS'] += os.pathsep + os.path.join( os.environ['TEXINPUTS'] += os.pathsep + os.path.join(
songbook.config['datadir'], self.songbook.config['datadir'],
'latex', 'latex',
) )
# pdflatex options if self.unsafe:
pdflatex_options = [] self._pdflatex_options.append("--shell-escape")
pdflatex_options.append("--shell-escape") # Lilypond compilation if not self.interactive:
if not interactive: self._pdflatex_options.append("-halt-on-error")
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
# Compilation
for step in steps: for step in steps:
if step == 'tex': if step == 'tex':
# Building .tex file from templates self.build_tex()
with codecs.open("{}.tex".format(basename), 'w', 'utf-8') as output:
songbook.write_tex(output)
elif step == 'pdf': elif step == 'pdf':
if subprocess.call(["pdflatex"] + pdflatex_options + [basename]): self.build_pdf()
raise errors.LatexCompilationError(basename)
elif step == 'sbx': elif step == 'sbx':
# Make index self.build_sbx()
sxd_files = glob.glob("%s_*.sxd" % basename) 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: for sxd_file in sxd_files:
logger.info("processing " + sxd_file) LOGGER.debug("Processing " + sxd_file)
idx = process_sxd(sxd_file) idx = process_sxd(sxd_file)
index_file = open(sxd_file[:-3] + "sbx", "w") 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:]
exit_code = subprocess.call(command, shell=True)
if exit_code: if exit_code:
raise errors.StepCommandError(command, exit_code) raise errors.StepCommandError(command, exit_code)
else:
# Unknown step name def clean(self):
raise errors.UnknownStep(step) """Clean (some) temporary files used during compilation.
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/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