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

50
patacrep/content/song.py

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

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

76
patacrep/files.py

@ -3,13 +3,17 @@
from contextlib import contextmanager
import fnmatch
import importlib
import logging
import os
import posixpath
def recursive_find(root_directory, pattern):
"""Recursively find files matching a pattern, from a root_directory.
LOGGER = logging.getLogger(__name__)
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):
return []
@ -17,8 +21,12 @@ def recursive_find(root_directory, pattern):
matches = []
with chdir(root_directory):
for root, __ignored, filenames in os.walk(os.curdir):
for filename in fnmatch.filter(filenames, pattern):
matches.append(os.path.join(root, filename))
for pattern in patterns:
for filename in fnmatch.filter(
filenames,
"*.{}".format(pattern),
):
matches.append(os.path.join(root, filename))
return matches
def relpath(path, start=None):
@ -59,3 +67,61 @@ def chdir(path):
os.chdir(olddir)
else:
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
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
from patacrep.latex.syntax import tex2plain, parse_song

23
patacrep/latex/syntax.py

@ -237,12 +237,21 @@ def tex2plain(string):
)
)
def parsesong(string, filename=None):
"""Parse song and return its 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)).parse(
string,
lexer=SongLexer().lexer,
).metadata
yacc.yacc(
module=Parser(filename),
write_tables=0,
debug=0,
).parse(
content,
lexer=SongLexer().lexer,
).metadata
)

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

@ -10,7 +10,7 @@ import pickle
import re
from patacrep.authors import processauthors
from patacrep.latex import parsesong
from patacrep import files, encoding
LOGGER = logging.getLogger(__name__)
@ -84,6 +84,12 @@ class Song(object):
"_version",
]
# Default data
DEFAULT_DATA = {
'@titles': [],
'@languages': [],
}
def __init__(self, datadir, subpath, config):
self.fullpath = os.path.join(datadir, subpath)
if datadir:
@ -110,7 +116,11 @@ class Song(object):
))
# 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.languages = self.data['@languages']
self.datadir = datadir
@ -149,6 +159,18 @@ class Song(object):
def __repr__(self):
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):
"""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:
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