Browse Source

Réorganisation en prévision du support des fichiers chordpro

Ref: #64
pull/70/head
Louis 10 years ago
committed by Luthaf
parent
commit
eefcbd2bd2
  1. 0
      patacrep/chordpro/__init__.py
  2. 3
      patacrep/content/__init__.py
  3. 61
      patacrep/content/song.py
  4. 3
      patacrep/content/sorted.py
  5. 98
      patacrep/files.py
  6. 20
      patacrep/latex/__init__.py
  7. 25
      patacrep/latex/syntax.py
  8. 73
      patacrep/songs/__init__.py
  9. 9
      patacrep/songs/chordpro/__init__.py
  10. 0
      patacrep/songs/chordpro/ast.py
  11. 0
      patacrep/songs/chordpro/lexer.py
  12. 4
      patacrep/songs/chordpro/parser.py
  13. 32
      patacrep/songs/latex/__init__.py

0
patacrep/chordpro/__init__.py

3
patacrep/content/__init__.py

@ -173,8 +173,7 @@ def process_content(content, config=None):
included in the .tex file. included in the .tex file.
""" """
contentlist = [] contentlist = []
plugins = config.get('_content_plugins', {}) plugins = files.load_plugins(config, ["content"], "CONTENT_PLUGINS")
keyword_re = re.compile(r'^ *(?P<keyword>\w*) *(\((?P<argument>.*)\))? *$') keyword_re = re.compile(r'^ *(?P<keyword>\w*) *(\((?P<argument>.*)\))? *$')
if not content: if not content:
content = [["song"]] content = [["song"]]

61
patacrep/content/song.py

@ -6,10 +6,35 @@ import os
from patacrep.content import process_content, ContentError from patacrep.content import process_content, ContentError
from patacrep import files, errors from patacrep import files, errors
from patacrep.songs import Song
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
class SongRenderer(Content):
"""Render a song in as a tex code."""
def __init__(self, song):
super().__init__()
self.song = song
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."""
return self.song.tex(output=context['filename'])
#pylint: disable=unused-argument #pylint: disable=unused-argument
def parse(keyword, argument, contentlist, config): def parse(keyword, argument, contentlist, config):
"""Parse data associated with keyword 'song'. """Parse data associated with keyword 'song'.
@ -23,6 +48,7 @@ def parse(keyword, argument, contentlist, config):
Return a list of Song() instances. Return a list of Song() instances.
""" """
plugins = files.load_plugins(config, ["songs"], "SONG_PARSERS")
if '_languages' not in config: if '_languages' not in config:
config['_languages'] = set() config['_languages'] = set()
songlist = [] songlist = []
@ -30,8 +56,11 @@ def parse(keyword, argument, contentlist, config):
for songdir in config['_songdir']: for songdir in config['_songdir']:
if contentlist: if contentlist:
break break
contentlist = files.recursive_find(songdir.fullpath, plugins.keys()) contentlist = [
filename
for filename
in files.recursive_find(songdir.fullpath, plugins.keys())
]
for elem in contentlist: for elem in contentlist:
before = len(songlist) before = len(songlist)
for songdir in config['_songdir']: for songdir in config['_songdir']:
@ -39,18 +68,24 @@ def parse(keyword, argument, contentlist, config):
continue continue
with files.chdir(songdir.datadir): with files.chdir(songdir.datadir):
for filename in glob.iglob(os.path.join(songdir.subpath, elem)): for filename in glob.iglob(os.path.join(songdir.subpath, elem)):
LOGGER.debug('Parsing file "{}"'.format(filename)) extension = filename.split(".")[-1]
try: if extension not in plugins:
renderer = plugins[filename.split('.')[-1]]
except KeyError:
LOGGER.warning(( LOGGER.warning((
'I do not know how to parse file "{}". Ignored.' 'File "{}" does not end with one of {}. Ignored.'
).format(os.path.join(songdir.datadir, filename)) ).format(
os.path.join(songdir.datadir, filename),
", ".join(["'.{}'".format(key) for key in plugins.keys()]),
)
) )
continue continue
song = renderer(songdir.datadir, filename, config) LOGGER.debug('Parsing file "{}"'.format(filename))
songlist.append(song) renderer = SongRenderer(plugins[extension](
config["_languages"].update(song.languages) songdir.datadir,
filename,
config,
))
songlist.append(renderer)
config["_languages"].update(renderer.song.languages)
if len(songlist) > before: if len(songlist) > before:
break break
if len(songlist) == before: if len(songlist) == before:
@ -69,8 +104,8 @@ CONTENT_PLUGINS = {'song': parse}
class OnlySongsError(ContentError): class OnlySongsError(ContentError):
"A list that should contain only songs also contain other type of content." "A list that should contain only songs also contain other type of content."
def __init__(self, not_songs): def __init__(self, not_songs):
super(OnlySongsError, self).__init__()
self.not_songs = not_songs self.not_songs = not_songs
super().__init__('song', str(self))
def __str__(self): def __str__(self):
return ( return (

3
patacrep/content/sorted.py

@ -40,8 +40,9 @@ def key_generator(sort):
- sort: the list of keys used to sort. - sort: the list of keys used to sort.
""" """
def ordered_song_keys(song): def ordered_song_keys(songrenderer):
"""Return the list of values used to sort the song.""" """Return the list of values used to sort the song."""
song = songrenderer.song
songkey = [] songkey = []
for key in sort: for key in sort:
if key == "@title": if key == "@title":

98
patacrep/files.py

@ -1,7 +1,7 @@
"""File system utilities.""" """File system utilities."""
from contextlib import contextmanager from contextlib import contextmanager
import glob import fnmatch
import importlib import importlib
import logging import logging
import os import os
@ -11,14 +11,10 @@ import sys
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
def recursive_find(root_directory, extensions): def recursive_find(root_directory, patterns):
"""Recursively find files with some extension, from a root_directory. """Recursively find files matching one of the patterns, from a root_directory.
Return a list of files matching those conditions. Return a list of files matching one of the patterns.
Arguments:
- `extensions`: list of accepted extensions.
- `root_directory`: root directory of the search.
""" """
if not os.path.isdir(root_directory): if not os.path.isdir(root_directory):
return [] return []
@ -27,8 +23,11 @@ def recursive_find(root_directory, extensions):
pattern = re.compile(r'.*\.({})$'.format('|'.join(extensions))) pattern = re.compile(r'.*\.({})$'.format('|'.join(extensions)))
with chdir(root_directory): with chdir(root_directory):
for root, __ignored, filenames in os.walk(os.curdir): for root, __ignored, filenames in os.walk(os.curdir):
for filename in filenames: for pattern in patterns:
if pattern.match(filename): for filename in fnmatch.filter(
filenames,
"*.{}".format(pattern),
):
matches.append(os.path.join(root, filename)) matches.append(os.path.join(root, filename))
return matches return matches
@ -71,29 +70,31 @@ def chdir(path):
else: else:
yield yield
def load_plugins(datadirs, subdir, variable, error): def load_plugins(config, root_modules, keyword):
"""Load all content plugins, and return a dictionary of those plugins. """Load all plugins, and return a dictionary of those plugins.
A plugin is a .py file, submodule of `subdir`, located in one of the
directories of `datadirs`. It contains a dictionary `variable`. The return
value is the union of the dictionaries of the loaded plugins.
Arguments: Arguments:
- datadirs: list of directories (as strings) in which files has to be - config: the configuration dictionary of the songbook
searched. - root_modules: the submodule in which plugins are to be searched, as a
- subdir: modules (as a list of strings) files has to be submodules of list of modules (e.g. ["some", "deep", "module"] for
(e.g. if `subdir` is `['first', 'second']`, search files are of the form "some.deep.module").
`first/second/*.py`. - keyword: attribute containing plugin information.
- variable: Name of the variable holding the dictionary.
- error: Error message raised if a key appears several times. Return value: a dictionary where:
- keys are the keywords ;
- values are functions triggered when this keyword is met.
""" """
# pylint: disable=star-args
plugins = {} plugins = {}
directory_list = ( directory_list = (
[ [
os.path.join(datadir, "python", *subdir) #pylint: disable=star-args os.path.join(datadir, "python", *root_modules)
for datadir in datadirs for datadir in config.get('datadir', [])
] ]
+ [os.path.dirname(__file__)] + [os.path.join(
os.path.dirname(__file__),
*root_modules
)]
) )
for directory in directory_list: for directory in directory_list:
if not os.path.exists(directory): if not os.path.exists(directory):
@ -102,31 +103,26 @@ def load_plugins(datadirs, subdir, variable, error):
directory directory
) )
continue continue
sys.path.append(directory) for (dirpath, __ignored, filenames) in os.walk(directory):
for name in glob.glob(os.path.join(directory, *(subdir + ['*.py']))): modules = ["patacrep"] + root_modules
if name.endswith(".py") and os.path.basename(name) != "__init__.py": if os.path.relpath(dirpath, directory) != ".":
if directory == os.path.dirname(__file__): modules.extend(os.path.relpath(dirpath, directory).split("/"))
plugin = importlib.import_module( for name in filenames:
'patacrep.{}.{}'.format( if name == "__init__.py":
".".join(subdir), modulename = []
os.path.basename(name[:-len('.py')]) elif name.endswith(".py"):
) modulename = [name[:-len('.py')]]
)
else: else:
plugin = importlib.import_module( continue
os.path.basename(name[:-len('.py')]) plugin = importlib.import_module(".".join(modules + modulename))
if hasattr(plugin, keyword):
for (key, value) in getattr(plugin, keyword).items():
if key in plugins:
LOGGER.warning(
"File %s: Keyword '%s' is already used. Ignored.",
relpath(os.path.join(dirpath, name)),
key,
) )
for (key, value) in getattr(plugin, variable, {}).items(): continue
if key in plugins: plugins[key] = value
LOGGER.warning(
error.format(
filename=relpath(name),
key=key,
)
)
continue
plugins[key] = value
del sys.path[-1]
return plugins return plugins

20
patacrep/latex/__init__.py

@ -1,19 +1,3 @@
"""Very simple LaTeX parser """Dumb and very very incomplete LaTeX parser."""
This module uses an LALR parser to try to parse LaTeX code. LaTeX language from patacrep.latex.syntax import tex2plain, parse_song
*cannot* be parsed by an LALR parser, so this is a very simple attemps, which
will work on simple cases, but not on complex ones.
"""
from patacrep.latex.syntax import tex2plain
from patacrep.latex.syntax import parsesong as syntax_parsesong
from patacrep import encoding
def parsesong(path, fileencoding=None):
"""Return a dictonary of data read from the latex file `path`.
"""
with encoding.open_read(path, encoding=fileencoding) as songfile:
data = syntax_parsesong(songfile.read(), path)
data['@path'] = path
return data

25
patacrep/latex/syntax.py

@ -245,12 +245,21 @@ def tex2plain(string):
) )
) )
def parsesong(string, filename=None): def parse_song(content, filename=None):
"""Parse song and return its metadata.""" """Parse some LaTeX code, expected to be a song.
return detex(
silent_yacc(module=Parser(filename)).parse(
string,
lexer=SongLexer().lexer,
).metadata
)
Arguments:
- content: the code to parse.
- filename: the name of file where content was read from. Used only to
display error messages.
"""
return detex(
yacc.yacc(
module=Parser(filename),
write_tables=0,
debug=0,
).parse(
content,
lexer=SongLexer().lexer,
).metadata
)

73
patacrep/songs/__init__.py

@ -9,7 +9,7 @@ import pickle
import re import re
from patacrep.authors import processauthors from patacrep.authors import processauthors
from patacrep.content import Content from patacrep import files, encoding
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
@ -95,6 +95,12 @@ class Song(Content):
"_version", "_version",
] ]
# Default data
DEFAULT_DATA = {
'@titles': [],
'@languages': [],
}
def __init__(self, datadir, subpath, config): def __init__(self, datadir, subpath, config):
self.fullpath = os.path.join(datadir, subpath) self.fullpath = os.path.join(datadir, subpath)
self.datadir = datadir self.datadir = datadir
@ -123,14 +129,14 @@ class Song(Content):
self.fullpath self.fullpath
)) ))
# Default values # Data extraction from the latex song
self.data = {} self.data = self.DEFAULT_DATA
self.titles = [] self.data['@path'] = self.fullpath
self.languages = [] self.data.update(self.parse(
self.authors = [] encoding.open_read(self.fullpath).read()
))
# Parsing and data processing self.titles = self.data['@titles']
self.parse() self.languages = self.data['@languages']
self.datadir = datadir self.datadir = datadir
self.unprefixed_titles = [ self.unprefixed_titles = [
unprefixed_title( unprefixed_title(
@ -169,49 +175,17 @@ class Song(Content):
def __repr__(self): def __repr__(self):
return repr((self.titles, self.data, self.fullpath)) return repr((self.titles, self.data, self.fullpath))
def begin_new_block(self, previous, __context): def tex(self, output): # pylint: disable=no-self-use, unused-argument
"""Return a boolean stating if a new block is to be created.""" """Return the LaTeX code rendering this song.
return not isinstance(previous, Song)
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):
"""Returns the TeX code rendering the song.
This function is to be defined by subclasses. Arguments:
- output: Name of the output file.
""" """
return '' return NotImplementedError()
def parse(self): def parse(self, content): # pylint: disable=no-self-use, unused-argument
"""Parse file `self.fullpath`. """Parse song, and return a dictionary of its data."""
return NotImplementedError()
This function is to be defined by subclasses.
It set the following attributes:
- titles: the list of (raw) titles. This list will be processed to
remove prefixes.
- languages: the list of languages used in the song, as languages
recognized by the LaTeX babel package.
- authors: the list of (raw) authors. This list will be processed to
'clean' it (see function :func:`patacrep.authors.processauthors`).
- data: song metadata. Used (among others) to sort the songs.
- cached: additional data that will be cached. Thus, data stored in
this attribute must be picklable.
"""
self.data = {}
self.titles = []
self.languages = []
self.authors = []
def unprefixed_title(title, prefixes): def unprefixed_title(title, prefixes):
"""Remove the first prefix of the list in the beginning of title (if any). """Remove the first prefix of the list in the beginning of title (if any).
@ -221,4 +195,3 @@ def unprefixed_title(title, prefixes):
if match: if match:
return match.group(2) return match.group(2)
return title return title

9
patacrep/songs/chordpro/__init__.py

@ -0,0 +1,9 @@
from patacrep.songs import Song
class ChordproSong(Song):
pass
SONG_PARSERS = {
'sgc': ChordproSong,
}

0
patacrep/chordpro/ast.py → patacrep/songs/chordpro/ast.py

0
patacrep/chordpro/lexer.py → patacrep/songs/chordpro/lexer.py

4
patacrep/chordpro/parser.py → patacrep/songs/chordpro/parser.py

@ -4,8 +4,8 @@
import logging import logging
import ply.yacc as yacc import ply.yacc as yacc
from patacrep.chordpro.lexer import tokens, ChordProLexer from patacrep.songs.chordpro.lexer import tokens, ChordProLexer
from patacrep.chordpro import ast from patacrep.songs.chordpro import ast
from patacrep.errors import SongbookError from patacrep.errors import SongbookError
LOGGER = logging.getLogger() LOGGER = logging.getLogger()

32
patacrep/songs/latex/__init__.py

@ -0,0 +1,32 @@
"""Very simple LaTeX parser
This module uses an LALR parser to try to parse LaTeX code. LaTeX language
*cannot* be parsed by an LALR parser, so this is a very simple attemps, which
will work on simple cases, but not on complex ones.
"""
import os
from patacrep import files
from patacrep.latex import parse_song
from patacrep.songs import Song
class LatexSong(Song):
"""LaTeX song parser."""
def parse(self, content):
"""Parse content, and return the dictinory of song data."""
return parse_song(content, self.fullpath)
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)
)))
SONG_PARSERS = {
'is': LatexSong,
'sg': LatexSong,
}
Loading…
Cancel
Save