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.
"""
contentlist = []
plugins = config.get('_content_plugins', {})
plugins = files.load_plugins(config, ["content"], "CONTENT_PLUGINS")
keyword_re = re.compile(r'^ *(?P<keyword>\w*) *(\((?P<argument>.*)\))? *$')
if not content:
content = [["song"]]

61
patacrep/content/song.py

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

3
patacrep/content/sorted.py

@ -40,8 +40,9 @@ def key_generator(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."""
song = songrenderer.song
songkey = []
for key in sort:
if key == "@title":

98
patacrep/files.py

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

25
patacrep/latex/syntax.py

@ -245,12 +245,21 @@ def tex2plain(string):
)
)
def parsesong(string, filename=None):
"""Parse song and return its metadata."""
return detex(
silent_yacc(module=Parser(filename)).parse(
string,
lexer=SongLexer().lexer,
).metadata
)
def parse_song(content, filename=None):
"""Parse some LaTeX code, expected to be a song.
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
from patacrep.authors import processauthors
from patacrep.content import Content
from patacrep import files, encoding
LOGGER = logging.getLogger(__name__)
@ -95,6 +95,12 @@ class Song(Content):
"_version",
]
# Default data
DEFAULT_DATA = {
'@titles': [],
'@languages': [],
}
def __init__(self, datadir, subpath, config):
self.fullpath = os.path.join(datadir, subpath)
self.datadir = datadir
@ -123,14 +129,14 @@ class Song(Content):
self.fullpath
))
# Default values
self.data = {}
self.titles = []
self.languages = []
self.authors = []
# Parsing and data processing
self.parse()
# Data extraction from the latex song
self.data = self.DEFAULT_DATA
self.data['@path'] = self.fullpath
self.data.update(self.parse(
encoding.open_read(self.fullpath).read()
))
self.titles = self.data['@titles']
self.languages = self.data['@languages']
self.datadir = datadir
self.unprefixed_titles = [
unprefixed_title(
@ -169,49 +175,17 @@ class Song(Content):
def __repr__(self):
return repr((self.titles, self.data, self.fullpath))
def begin_new_block(self, previous, __context):
"""Return a boolean stating if a new block is to be created."""
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.
def tex(self, output): # pylint: disable=no-self-use, unused-argument
"""Return the LaTeX code rendering this song.
This function is to be defined by subclasses.
Arguments:
- output: Name of the output file.
"""
return ''
return NotImplementedError()
def parse(self):
"""Parse file `self.fullpath`.
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 parse(self, content): # pylint: disable=no-self-use, unused-argument
"""Parse song, and return a dictionary of its data."""
return NotImplementedError()
def unprefixed_title(title, prefixes):
"""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:
return match.group(2)
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 ply.yacc as yacc
from patacrep.chordpro.lexer import tokens, ChordProLexer
from patacrep.chordpro import ast
from patacrep.songs.chordpro.lexer import tokens, ChordProLexer
from patacrep.songs.chordpro import ast
from patacrep.errors import SongbookError
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