Browse Source

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

Ref: #64
pull/70/head
Louis 10 years ago
parent
commit
a2e5c476e5
  1. 0
      patacrep/chordpro/__init__.py
  2. 49
      patacrep/content/__init__.py
  3. 50
      patacrep/content/song.py
  4. 3
      patacrep/content/sorted.py
  5. 76
      patacrep/files.py
  6. 21
      patacrep/latex/__init__.py
  7. 23
      patacrep/latex/syntax.py
  8. 28
      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

49
patacrep/content/__init__.py

@ -134,53 +134,6 @@ class ContentError(SongbookError):
def __str__(self): def __str__(self):
return "Content: {}: {}".format(self.keyword, self.message) return "Content: {}: {}".format(self.keyword, self.message)
def load_plugins(config):
"""Load all content plugins, and return a dictionary of those plugins.
Return value: a dictionary where:
- keys are the keywords ;
- values are functions triggered when this keyword is met.
"""
plugins = {}
directory_list = (
[
os.path.join(datadir, "python", "content")
for datadir in config.get('datadir', [])
]
+ [os.path.dirname(__file__)]
)
for directory in directory_list:
if not os.path.exists(directory):
LOGGER.debug(
"Ignoring non-existent directory '%s'.",
directory
)
continue
sys.path.append(directory)
for name in glob.glob(os.path.join(directory, '*.py')):
if name.endswith(".py") and os.path.basename(name) != "__init__.py":
if directory == os.path.dirname(__file__):
plugin = importlib.import_module(
'patacrep.content.{}'.format(
os.path.basename(name[:-len('.py')])
)
)
else:
plugin = importlib.import_module(
os.path.basename(name[:-len('.py')])
)
for (key, value) in plugin.CONTENT_PLUGINS.items():
if key in plugins:
LOGGER.warning(
"File %s: Keyword '%s' is already used. Ignored.",
files.relpath(name),
key,
)
continue
plugins[key] = value
del sys.path[-1]
return plugins
@jinja2.contextfunction @jinja2.contextfunction
def render_content(context, content): def render_content(context, content):
"""Render the content of the songbook as a LaTeX code. """Render the content of the songbook as a LaTeX code.
@ -224,7 +177,7 @@ def process_content(content, config=None):
included in the .tex file. included in the .tex file.
""" """
contentlist = [] contentlist = []
plugins = load_plugins(config) 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"]]

50
patacrep/content/song.py

@ -10,12 +10,15 @@ import os
from patacrep.content import Content, process_content, ContentError from patacrep.content import Content, 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, Song): class SongRenderer(Content):
"""Render a song in the .tex file.""" """Render a song in as a tex code."""
def __init__(self, song):
super().__init__()
self.song = song
def begin_new_block(self, previous, __context): def begin_new_block(self, previous, __context):
"""Return a boolean stating if a new block is to be created.""" """Return a boolean stating if a new block is to be created."""
@ -34,11 +37,7 @@ class SongRenderer(Content, Song):
def render(self, context): def render(self, context):
"""Return the string that will render the song.""" """Return the string that will render the song."""
return r'\input{{{}}}'.format(files.path2posix( return self.song.tex(output=context['filename'])
files.relpath(
self.fullpath,
os.path.dirname(context['filename'])
)))
#pylint: disable=unused-argument #pylint: disable=unused-argument
def parse(keyword, argument, contentlist, config): def parse(keyword, argument, contentlist, config):
@ -53,6 +52,7 @@ def parse(keyword, argument, contentlist, config):
Return a list of SongRenderer() instances. Return a list of SongRenderer() 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 = []
@ -60,13 +60,10 @@ def parse(keyword, argument, contentlist, config):
if contentlist: if contentlist:
break break
contentlist = [ contentlist = [
filename filename
for filename for filename
in ( in files.recursive_find(songdir.fullpath, plugins.keys())
files.recursive_find(songdir.fullpath, "*.sg") ]
+ files.recursive_find(songdir.fullpath, "*.is")
)
]
for elem in contentlist: for elem in contentlist:
before = len(songlist) before = len(songlist)
for songdir in config['_songdir']: for songdir in config['_songdir']:
@ -74,23 +71,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)):
if not ( extension = filename.split(".")[-1]
filename.endswith('.sg') or if extension not in plugins:
filename.endswith('.is')
):
LOGGER.warning(( LOGGER.warning((
'File "{}" is not a ".sg" or ".is" 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
LOGGER.debug('Parsing file "{}"'.format(filename)) LOGGER.debug('Parsing file "{}"'.format(filename))
song = SongRenderer( renderer = SongRenderer(plugins[extension](
songdir.datadir, songdir.datadir,
filename, filename,
config, config,
) ))
songlist.append(song) songlist.append(renderer)
config["_languages"].update(song.languages) config["_languages"].update(renderer.song.languages)
if len(songlist) > before: if len(songlist) > before:
break break
if len(songlist) == before: if len(songlist) == before:
@ -109,8 +107,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

@ -43,8 +43,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":

76
patacrep/files.py

@ -3,13 +3,17 @@
from contextlib import contextmanager from contextlib import contextmanager
import fnmatch import fnmatch
import importlib
import logging
import os import os
import posixpath import posixpath
def recursive_find(root_directory, pattern): LOGGER = logging.getLogger(__name__)
"""Recursively find files matching a pattern, from a root_directory.
Return a list of files matching the pattern. def recursive_find(root_directory, patterns):
"""Recursively find files matching one of the patterns, from a root_directory.
Return a list of files matching one of the patterns.
""" """
if not os.path.isdir(root_directory): if not os.path.isdir(root_directory):
return [] return []
@ -17,8 +21,12 @@ def recursive_find(root_directory, pattern):
matches = [] matches = []
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 fnmatch.filter(filenames, pattern): for pattern in patterns:
matches.append(os.path.join(root, filename)) for filename in fnmatch.filter(
filenames,
"*.{}".format(pattern),
):
matches.append(os.path.join(root, filename))
return matches return matches
def relpath(path, start=None): def relpath(path, start=None):
@ -59,3 +67,61 @@ def chdir(path):
os.chdir(olddir) os.chdir(olddir)
else: else:
yield yield
def load_plugins(config, root_modules, keyword):
"""Load all plugins, and return a dictionary of those plugins.
Arguments:
- 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", *root_modules)
for datadir in config.get('datadir', [])
]
+ [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
)
continue
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:
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,
)
continue
plugins[key] = value
return plugins

21
patacrep/latex/__init__.py

@ -1,20 +1,3 @@
# -*- coding: utf-8 -*- """Dumb and very very incomplete LaTeX parser."""
"""Very simple LaTeX parser from patacrep.latex.syntax import tex2plain, parse_song
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):
"""Return a dictonary of data read from the latex file `path`.
"""
data = syntax_parsesong(encoding.open_read(path).read(), path)
data['@path'] = path
return data

23
patacrep/latex/syntax.py

@ -237,12 +237,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.
Arguments:
- content: the code to parse.
- filename: the name of file where content was read from. Used only to
display error messages.
"""
return detex( return detex(
yacc.yacc(module=Parser(filename)).parse( yacc.yacc(
string, module=Parser(filename),
lexer=SongLexer().lexer, write_tables=0,
).metadata debug=0,
).parse(
content,
lexer=SongLexer().lexer,
).metadata
) )

28
patacrep/songs.py → patacrep/songs/__init__.py

@ -10,7 +10,7 @@ import pickle
import re import re
from patacrep.authors import processauthors from patacrep.authors import processauthors
from patacrep.latex import parsesong from patacrep import files, encoding
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
@ -84,6 +84,12 @@ class Song(object):
"_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)
if datadir: if datadir:
@ -110,7 +116,11 @@ class Song(object):
)) ))
# Data extraction from the latex song # Data extraction from the latex song
self.data = parsesong(self.fullpath) 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.titles = self.data['@titles']
self.languages = self.data['@languages'] self.languages = self.data['@languages']
self.datadir = datadir self.datadir = datadir
@ -149,6 +159,18 @@ class Song(object):
def __repr__(self): def __repr__(self):
return repr((self.titles, self.data, self.fullpath)) return repr((self.titles, self.data, self.fullpath))
def tex(self, output): # pylint: disable=no-self-use, unused-argument
"""Return the LaTeX code rendering this song.
Arguments:
- output: Name of the output file.
"""
return NotImplementedError()
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): 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).
""" """
@ -157,5 +179,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