Browse Source

Merge pull request #75 from patacrep/tox

Using tox for tests
pull/76/head
Luthaf 10 years ago
parent
commit
688f6c70ac
  1. 2
      .gitignore
  2. 9
      .travis.yml
  3. 13
      README.rst
  4. 2
      Requirements.txt
  5. 58
      patacrep/authors.py
  6. 84
      patacrep/build.py
  7. 8
      patacrep/content/cwd.py
  8. 12
      patacrep/content/include.py
  9. 24
      patacrep/content/section.py
  10. 27
      patacrep/content/song.py
  11. 12
      patacrep/content/songsection.py
  12. 8
      patacrep/content/sorted.py
  13. 10
      patacrep/content/tex.py
  14. 10
      patacrep/encoding.py
  15. 29
      patacrep/errors.py
  16. 30
      patacrep/files.py
  17. 34
      patacrep/index.py
  18. 12
      patacrep/latex/ast.py
  19. 8
      patacrep/latex/detex.py
  20. 54
      patacrep/latex/lexer.py
  21. 39
      patacrep/latex/syntax.py
  22. 111
      patacrep/songbook.py
  23. 54
      patacrep/songs/__init__.py
  24. 16
      patacrep/songs/chordpro/__init__.py
  25. 36
      patacrep/songs/chordpro/ast.py
  26. 2
      patacrep/songs/chordpro/lexer.py
  27. 50
      patacrep/songs/chordpro/syntax.py
  28. 4
      patacrep/songs/chordpro/test/test_parser.py
  29. 8
      patacrep/songs/latex/__init__.py
  30. 33
      patacrep/songs/syntax.py
  31. 69
      patacrep/templates.py
  32. 5
      pylintrc
  33. 15
      tox.ini

2
.gitignore

@ -13,3 +13,5 @@ dist
*.pdf
*.pyc
*.tar.gz
.eggs
.tox

9
.travis.yml

@ -0,0 +1,9 @@
git:
depth: 1
language: python
python:
- 3.4
install:
- pip install tox
script:
- tox

13
README.rst

@ -1,6 +1,8 @@
Patacrep, a songbook compilation chain
======================================
|sources| |build| |pypi| |documentation| |license|
This package provides a compilation toolchain that produce LaTeX
songbook using the LaTeX songs package. A new LaTeX document class is
provided to allow specific customisation and new command like embedded
@ -61,3 +63,14 @@ Contact & Forums
----------------
* http://www.patacrep.com/forum
.. |documentation| image:: http://readthedocs.org/projects/patacrep/badge
:target: http://patacrep.readthedocs.org
.. |pypi| image:: https://img.shields.io/pypi/v/patacrep.svg
:target: http://pypi.python.org/pypi/patacrep
.. |license| image:: https://img.shields.io/pypi/l/patacrep.svg
:target: http://www.gnu.org/licenses/gpl-2.0.html
.. |sources| image:: https://img.shields.io/badge/sources-patacrep-brightgreen.svg
:target: http://github.com/patacrep/patacrep
.. |build| image:: https://travis-ci.org/patacrep/patacrep.svg?branch=master
:target: https://travis-ci.org/patacrep/patacrep

2
Requirements.txt

@ -1,4 +1,4 @@
ply
Jinja2==2.7.3
argparse==1.2.1
chardet==2.2.1
unidecode>=0.04.16

58
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."""

84
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)

8
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

12
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)

24
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([

27
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

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

8
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

10
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))

10
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

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

30
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

34
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'])

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

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

54
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','

39
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

111
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.")

54
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))

16
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,

36
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)),
)

2
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_]+'

50
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,
)

4
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

8
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,

33
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),
)
)

69
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)

5
pylintrc

@ -0,0 +1,5 @@
[VARIABLES]
dummy-variables-rgx=_|dummy
[MESSAGES CONTROL]
disable= logging-format-interpolation

15
tox.ini

@ -0,0 +1,15 @@
# To perform those tests, install `tox` and run "tox" from this directory.
[tox]
# Uncomment to use more python versions
#envlist = py26, py27, py32, py34, lint
envlist = py34, lint
[testenv]
commands = {envpython} setup.py test
deps =
[testenv:lint]
basepython=python3.4
deps=pylint
commands=pylint patacrep --rcfile=pylintrc
Loading…
Cancel
Save