mirror of https://github.com/patacrep/patacrep.git
Luthaf
10 years ago
86 changed files with 1541 additions and 188 deletions
@ -0,0 +1,51 @@ |
|||
{language : english} |
|||
{columns : 2} |
|||
{ title : Greensleeves} |
|||
{ title : Un sous titre} |
|||
{artist: Traditionnel} |
|||
{artist: Prénom Nom} |
|||
{cover : traditionnel } |
|||
{album :Angleterre} |
|||
|
|||
{partition : greensleeves.ly} |
|||
|
|||
|
|||
A[Am]las, my love, ye [G]do me wrong |
|||
To [Am]cast me oft dis[E]curteously |
|||
And [Am]I have loved [G]you so long |
|||
De[Am]lighting [E]in your [Am]companie |
|||
|
|||
{start_of_chorus} |
|||
[C]Greensleeves was [G]all my joy |
|||
[Am]Greensleeves was [E]my delight |
|||
[C]Greensleeves was my [G]heart of gold |
|||
And [Am]who but [E]Ladie [Am]Greensleeves |
|||
{end_of_chorus} |
|||
|
|||
I [Am]have been ready [G]at your hand |
|||
To [Am]grant what ever [E]you would crave |
|||
I [Am]have both waged [G]life and land |
|||
Your [Am]love and [E]good will [Am]for to have |
|||
|
|||
I [Am]bought thee kerchers [G]to thy head |
|||
That [Am]were wrought fine and [E]gallantly |
|||
I [Am]kept thee both at [G]boord and bed |
|||
Which [Am]cost my [E]purse well [Am]favouredly |
|||
|
|||
I [Am]bought thee peticotes [G]of the best |
|||
The [Am]cloth so fine as [E]fine might be |
|||
I [Am]gave thee jewels [G]for thy chest |
|||
And [Am]all this [E]cost I [Am]spent on thee |
|||
|
|||
{c:test of comment} |
|||
|
|||
{gc: test of guitar comment} |
|||
|
|||
{image: traditionnel} |
|||
|
|||
Thy [Am]smock of silke, both [G]faire and white |
|||
With [Am]gold embrodered [E]gorgeously |
|||
Thy [Am]peticote of [G]sendall right |
|||
And [Am]this I [E]bought thee [Am]gladly |
|||
|
|||
|
@ -1,19 +1,8 @@ |
|||
"""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 |
|||
|
@ -0,0 +1,49 @@ |
|||
"""Chordpro parser""" |
|||
|
|||
from jinja2 import Environment, FileSystemLoader |
|||
import pkg_resources |
|||
import os |
|||
|
|||
from patacrep import encoding, files |
|||
from patacrep.songs import Song |
|||
from patacrep.songs.chordpro.syntax import parse_song |
|||
from patacrep.templates import TexRenderer |
|||
|
|||
class ChordproSong(Song): |
|||
"""Chordpros song parser.""" |
|||
|
|||
def parse(self, config): |
|||
"""Parse content, and return the dictinory of song data.""" |
|||
with encoding.open_read(self.fullpath, encoding=self.encoding) as song: |
|||
song = parse_song(song.read(), self.fullpath) |
|||
self.authors = song.authors |
|||
self.titles = song.titles |
|||
self.languages = song.get_directives('language') |
|||
self.data = dict([meta.as_tuple for meta in song.meta]) |
|||
self.cached = { |
|||
'song': song, |
|||
} |
|||
|
|||
def tex(self, output): |
|||
context = { |
|||
'language': self.cached['song'].get_directive('language', self.config['lang']), |
|||
'columns': self.cached['song'].get_directive('columns', 1), |
|||
"path": files.relpath(self.fullpath, os.path.dirname(output)), |
|||
"titles": r"\\".join(self.titles), |
|||
"authors": ", ".join(["{} {}".format(name[1], name[0]) for name in self.authors]), |
|||
"metadata": self.data, |
|||
"beginsong": self.cached['song'].meta_beginsong(), |
|||
"content": self.cached['song'].content, |
|||
} |
|||
return TexRenderer( |
|||
template="chordpro.tex", |
|||
encoding='utf8', |
|||
texenv=Environment(loader=FileSystemLoader(os.path.join( |
|||
os.path.abspath(pkg_resources.resource_filename(__name__, 'data')), |
|||
'latex' |
|||
))), |
|||
).template.render(context) |
|||
|
|||
SONG_PARSERS = { |
|||
'sgc': ChordproSong, |
|||
} |
@ -0,0 +1,410 @@ |
|||
"""Abstract Syntax Tree for ChordPro code.""" |
|||
|
|||
# pylint: disable=too-few-public-methods |
|||
|
|||
import functools |
|||
import logging |
|||
import os |
|||
|
|||
LOGGER = logging.getLogger() |
|||
|
|||
def _indent(string): |
|||
"""Return and indented version of argument.""" |
|||
return "\n".join([" {}".format(line) for line in string.split('\n')]) |
|||
|
|||
#: List of properties that are to be displayed in the flow of the song (not as |
|||
#: metadata at the beginning or end of song. |
|||
INLINE_PROPERTIES = { |
|||
"partition", |
|||
"comment", |
|||
"guitar_comment", |
|||
"image", |
|||
} |
|||
|
|||
#: List of properties that are listed in the `\beginsong` LaTeX directive. |
|||
BEGINSONG_PROPERTIES = { |
|||
"album", |
|||
"copyright", |
|||
"cov", |
|||
"vcov", |
|||
"tag", |
|||
} |
|||
|
|||
#: Some directive have alternative names. For instance `{title: Foo}` and `{t: |
|||
#: Foo}` are equivalent. |
|||
DIRECTIVE_SHORTCUTS = { |
|||
"t": "title", |
|||
"st": "subtitle", |
|||
"a": "album", |
|||
"by": "artist", |
|||
"c": "comment", |
|||
"gc": "guitar_comment", |
|||
"cover": "cov", |
|||
"vcover": "vcov", |
|||
} |
|||
|
|||
def directive_name(text): |
|||
"""Return name of the directive, considering eventual shortcuts.""" |
|||
return DIRECTIVE_SHORTCUTS.get(text, text) |
|||
|
|||
|
|||
class AST: |
|||
"""Generic object representing elements of the song.""" |
|||
_template = None |
|||
inline = False |
|||
|
|||
def template(self, extension): |
|||
"""Return the template to be used to render this object.""" |
|||
if self._template is None: |
|||
LOGGER.warning("No template defined for {}.".format(self.__class__)) |
|||
base = "error" |
|||
else: |
|||
base = self._template |
|||
return "content_{}.{}".format(base, extension) |
|||
|
|||
class Line(AST): |
|||
"""A line is a sequence of (possibly truncated) words, spaces and chords.""" |
|||
|
|||
_template = "line" |
|||
|
|||
def __init__(self): |
|||
super().__init__() |
|||
self.line = [] |
|||
|
|||
def prepend(self, data): |
|||
"""Add an object at the beginning of line.""" |
|||
self.line.insert(0, data) |
|||
return self |
|||
|
|||
def __str__(self): |
|||
return "".join([str(item) for item in self.line]) |
|||
|
|||
def strip(self): |
|||
"""Remove spaces at the beginning and end of line.""" |
|||
while True: |
|||
if not self.line: |
|||
return self |
|||
if isinstance(self.line[0], Space): |
|||
del self.line[0] |
|||
continue |
|||
if isinstance(self.line[-1], Space): |
|||
del self.line[-1] |
|||
continue |
|||
return self |
|||
|
|||
class LineElement(AST): |
|||
"""Something present on a line.""" |
|||
pass |
|||
|
|||
class Word(LineElement): |
|||
"""A chunk of word.""" |
|||
_template = "word" |
|||
|
|||
def __init__(self, value): |
|||
super().__init__() |
|||
self.value = value |
|||
|
|||
def __str__(self): |
|||
return self.value |
|||
|
|||
class Space(LineElement): |
|||
"""A space between words""" |
|||
_template = "space" |
|||
|
|||
def __init__(self): |
|||
super().__init__() |
|||
|
|||
def __str__(self): |
|||
return " " |
|||
|
|||
class Chord(LineElement): |
|||
"""A chord.""" |
|||
|
|||
_template = "chord" |
|||
|
|||
def __init__(self, value): |
|||
super().__init__() |
|||
self.value = value |
|||
|
|||
def __str__(self): |
|||
return "[{}]".format(self.value) |
|||
|
|||
class Verse(AST): |
|||
"""A verse (or bridge, or chorus)""" |
|||
_template = "verse" |
|||
type = "verse" |
|||
inline = True |
|||
|
|||
def __init__(self): |
|||
super().__init__() |
|||
self.lines = [] |
|||
|
|||
def prepend(self, data): |
|||
"""Add data at the beginning of verse.""" |
|||
self.lines.insert(0, data) |
|||
return self |
|||
|
|||
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])), |
|||
) |
|||
|
|||
class Chorus(Verse): |
|||
"""Chorus""" |
|||
type = 'chorus' |
|||
|
|||
class Bridge(Verse): |
|||
"""Bridge""" |
|||
type = 'bridge' |
|||
|
|||
class Song(AST): |
|||
r"""A song |
|||
|
|||
Attributes: |
|||
- content: the song content, as a list of objects `foo` such that |
|||
`foo.inline` is True. |
|||
- titles: The list of titles |
|||
- language: The language (if set), None otherwise |
|||
- authors: The list of authors |
|||
- meta_beginsong: The list of directives that are to be set in the |
|||
`\beginsong{}` LaTeX directive. |
|||
- meta: Every other metadata. |
|||
""" |
|||
|
|||
#: Some directives are added to the song using special methods. |
|||
METADATA_TYPE = { |
|||
"title": "add_title", |
|||
"subtitle": "add_subtitle", |
|||
"artist": "add_author", |
|||
"key": "add_key", |
|||
} |
|||
|
|||
#: Some directives have to be processed before being considered. |
|||
PROCESS_DIRECTIVE = { |
|||
"cov": "_process_relative", |
|||
"partition": "_process_relative", |
|||
"image": "_process_relative", |
|||
} |
|||
|
|||
def __init__(self, filename): |
|||
super().__init__() |
|||
self.content = [] |
|||
self.meta = [] |
|||
self._authors = [] |
|||
self._titles = [] |
|||
self._subtitles = [] |
|||
self._keys = [] |
|||
self.filename = filename |
|||
|
|||
def add(self, data): |
|||
"""Add an element to the song""" |
|||
if isinstance(data, Directive): |
|||
# Some directives are preprocessed |
|||
name = directive_name(data.keyword) |
|||
if name in self.PROCESS_DIRECTIVE: |
|||
data = getattr(self, self.PROCESS_DIRECTIVE[name])(data) |
|||
|
|||
if data is None: |
|||
# New line |
|||
if not (self.content and isinstance(self.content[0], Newline)): |
|||
self.content.insert(0, Newline()) |
|||
elif isinstance(data, Line): |
|||
# Add a new line, maybe in the current verse. |
|||
if not (self.content and isinstance(self.content[0], Verse)): |
|||
self.content.insert(0, Verse()) |
|||
self.content[0].prepend(data.strip()) |
|||
elif data.inline: |
|||
# Add an object in the content of the song. |
|||
self.content.insert(0, data) |
|||
elif isinstance(data, Directive): |
|||
# Add a metadata directive. Some of them are added using special |
|||
# methods listed in ``METADATA_TYPE``. |
|||
name = directive_name(data.keyword) |
|||
if name in self.METADATA_TYPE: |
|||
getattr(self, self.METADATA_TYPE[name])(*data.as_tuple) |
|||
else: |
|||
self.meta.append(data) |
|||
else: |
|||
raise Exception() |
|||
return self |
|||
|
|||
def str_meta(self): |
|||
"""Return an iterator over *all* metadata, as strings.""" |
|||
for title in self.titles: |
|||
yield "{{title: {}}}".format(title) |
|||
for author in self.authors: |
|||
yield "{{by: {}}}".format(author) |
|||
for key in sorted(self.keys): |
|||
yield "{{key: {}}}".format(str(key)) |
|||
for key in sorted(self.meta): |
|||
yield str(key) |
|||
|
|||
def __str__(self): |
|||
return ( |
|||
"\n".join(self.str_meta()).strip() |
|||
+ |
|||
"\n========\n" |
|||
+ |
|||
"\n".join([str(item) for item in self.content]).strip() |
|||
) |
|||
|
|||
|
|||
def add_title(self, __ignored, title): |
|||
"""Add a title""" |
|||
self._titles.insert(0, title) |
|||
|
|||
def add_subtitle(self, __ignored, title): |
|||
"""Add a subtitle""" |
|||
self._subtitles.insert(0, title) |
|||
|
|||
@property |
|||
def titles(self): |
|||
"""Return the list of titles (and subtitles).""" |
|||
return self._titles + self._subtitles |
|||
|
|||
def add_author(self, __ignored, title): |
|||
"""Add an auhor.""" |
|||
self._authors.insert(0, title) |
|||
|
|||
@property |
|||
def authors(self): |
|||
"""Return the list of (raw) authors.""" |
|||
return self._authors |
|||
|
|||
def get_directive(self, key, default=None): |
|||
"""Return the first directive with a given key.""" |
|||
for directive in self.meta: |
|||
if directive.keyword == directive_name(key): |
|||
return directive.argument |
|||
return default |
|||
|
|||
def get_directives(self, key): |
|||
"""Return the list of directives with a given key.""" |
|||
values = [] |
|||
for directive in self.meta: |
|||
if directive.keyword == directive_name(key): |
|||
values.append(directive.argument) |
|||
return values |
|||
|
|||
def add_key(self, __ignored, argument): |
|||
"""Add a new {key: foo: bar} directive.""" |
|||
key, *argument = argument.split(":") |
|||
self._keys.append(Directive( |
|||
key.strip(), |
|||
":".join(argument).strip(), |
|||
)) |
|||
|
|||
@property |
|||
def keys(self): |
|||
"""Return the list of keys. |
|||
|
|||
That is, directive that where given of the form ``{key: foo: bar}``. |
|||
""" |
|||
return self._keys |
|||
|
|||
def meta_beginsong(self): |
|||
r"""Return the meta information to be put in \beginsong.""" |
|||
for directive in BEGINSONG_PROPERTIES: |
|||
if self.get_directive(directive) is not None: |
|||
yield (directive, self.get_directive(directive)) |
|||
for (key, value) in self.keys: |
|||
yield (key, value) |
|||
|
|||
|
|||
def _process_relative(self, directive): |
|||
"""Return the directive, in which the argument is given relative to file |
|||
|
|||
This argument is expected to be a path (as a string). |
|||
""" |
|||
return Directive( |
|||
directive.keyword, |
|||
os.path.join( |
|||
os.path.dirname(self.filename), |
|||
directive.argument, |
|||
), |
|||
) |
|||
|
|||
class Newline(AST): |
|||
"""New line""" |
|||
_template = "newline" |
|||
|
|||
def __str__(self): |
|||
return "" |
|||
|
|||
@functools.total_ordering |
|||
class Directive(AST): |
|||
"""A directive""" |
|||
|
|||
def __init__(self, keyword="", argument=None): |
|||
super().__init__() |
|||
self._keyword = None |
|||
self.keyword = keyword |
|||
self.argument = argument |
|||
|
|||
@property |
|||
def _template(self): |
|||
"""Name of the template to use to render this keyword. |
|||
|
|||
This only applies if ``self.inline == True`` |
|||
""" |
|||
return self.keyword |
|||
|
|||
@property |
|||
def keyword(self): |
|||
"""Keyword of the directive.""" |
|||
return self._keyword |
|||
|
|||
@property |
|||
def inline(self): |
|||
"""True iff this directive is to be rendered in the flow on the song. |
|||
""" |
|||
return self.keyword in INLINE_PROPERTIES |
|||
|
|||
@keyword.setter |
|||
def keyword(self, value): |
|||
"""self.keyword setter |
|||
|
|||
Replace keyword by its canonical name if it is a shortcut. |
|||
""" |
|||
self._keyword = directive_name(value.strip()) |
|||
|
|||
def __str__(self): |
|||
if self.argument is not None: |
|||
return "{{{}: {}}}".format( |
|||
self.keyword, |
|||
self.argument, |
|||
) |
|||
else: |
|||
return "{{{}}}".format(self.keyword) |
|||
|
|||
@property |
|||
def as_tuple(self): |
|||
"""Return the directive as a tuple.""" |
|||
return (self.keyword, self.argument) |
|||
|
|||
def __eq__(self, other): |
|||
return self.as_tuple == other.as_tuple |
|||
|
|||
def __lt__(self, other): |
|||
return self.as_tuple < other.as_tuple |
|||
|
|||
class Tab(AST): |
|||
"""Tablature""" |
|||
|
|||
inline = True |
|||
|
|||
def __init__(self): |
|||
super().__init__() |
|||
self.content = [] |
|||
|
|||
def prepend(self, data): |
|||
"""Add an element at the beginning of content.""" |
|||
self.content.insert(0, data) |
|||
return self |
|||
|
|||
def __str__(self): |
|||
return '{{start_of_tab}}\n{}\n{{end_of_tab}}'.format( |
|||
_indent("\n".join(self.content)), |
|||
) |
|||
|
@ -0,0 +1,27 @@ |
|||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% |
|||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% |
|||
% ((path)) |
|||
|
|||
(* if language is defined *) |
|||
\selectlanguage{((language))} |
|||
(* endif *) |
|||
\songcolumns{((metadata.columns))} |
|||
|
|||
\beginsong{((titles))}[ |
|||
by={((authors))}, |
|||
(* for (key, argument) in beginsong *) |
|||
((key))={((argument))}, |
|||
(* endfor *) |
|||
] |
|||
|
|||
(* if (metadata.cov is defined) or (metadata.vcov is defined) *) |
|||
\cover |
|||
(* endif *) |
|||
|
|||
(* for content in content *) |
|||
(* include content.template("tex") *) |
|||
(* endfor *) |
|||
|
|||
\endsong |
|||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% |
|||
|
@ -0,0 +1 @@ |
|||
\[(( content.value ))] |
@ -0,0 +1 @@ |
|||
\textnote{(( content.argument ))} |
@ -0,0 +1,3 @@ |
|||
|
|||
ERROR : Template not found for \verb+(( content.__class__))+. See the logs for details. |
|||
|
@ -0,0 +1 @@ |
|||
\musicnote{(( content.argument ))} |
@ -0,0 +1 @@ |
|||
\image{(( content.argument ))} |
@ -0,0 +1 @@ |
|||
(* for content in content.line *)(* include content.template("tex") *)(* endfor *) |
@ -0,0 +1,2 @@ |
|||
|
|||
|
@ -0,0 +1 @@ |
|||
\lilypond{((content.argument))} |
@ -0,0 +1 @@ |
|||
|
@ -0,0 +1,7 @@ |
|||
\begin{(( content.type ))} |
|||
(* for content in content.lines *) |
|||
(* include content.template("tex") *) |
|||
|
|||
(* endfor *) |
|||
\end{(( content.type ))} |
|||
|
@ -0,0 +1 @@ |
|||
(( content.value )) |
@ -0,0 +1,161 @@ |
|||
"""ChordPro lexer""" |
|||
|
|||
import logging |
|||
import ply.lex as lex |
|||
|
|||
LOGGER = logging.getLogger() |
|||
|
|||
#pylint: disable=invalid-name |
|||
tokens = ( |
|||
'LBRACE', |
|||
'RBRACE', |
|||
'CHORD', |
|||
'NEWLINE', |
|||
'COLON', |
|||
'WORD', |
|||
'SPACE', |
|||
'TEXT', |
|||
'KEYWORD', |
|||
'SOC', |
|||
'EOC', |
|||
'SOB', |
|||
'EOB', |
|||
'SOT', |
|||
'EOT', |
|||
) |
|||
|
|||
class ChordProLexer: |
|||
"""ChordPro Lexer class""" |
|||
# pylint: disable=too-many-public-methods |
|||
|
|||
tokens = tokens |
|||
|
|||
states = ( |
|||
('chord', 'exclusive'), |
|||
('directive', 'exclusive'), |
|||
('directiveargument', 'exclusive'), |
|||
('tablature', 'exclusive'), |
|||
) |
|||
|
|||
t_SPACE = r'[ \t]+' |
|||
|
|||
t_chord_CHORD = r'[A-G7#m]+' # TODO This can be refined |
|||
|
|||
t_directive_SPACE = r'[ \t]+' |
|||
t_directive_KEYWORD = r'[a-zA-Z_]+' |
|||
t_directiveargument_TEXT = r'[^}]+' |
|||
|
|||
@staticmethod |
|||
def t_SOC(token): |
|||
r'{(soc|start_of_chorus)}' |
|||
return token |
|||
|
|||
@staticmethod |
|||
def t_EOC(token): |
|||
r'{(eoc|end_of_chorus)}' |
|||
return token |
|||
|
|||
@staticmethod |
|||
def t_SOB(token): |
|||
r'{(sob|start_of_bridge)}' |
|||
return token |
|||
|
|||
@staticmethod |
|||
def t_EOB(token): |
|||
r'{(eob|end_of_bridge)}' |
|||
return token |
|||
|
|||
def t_SOT(self, token): |
|||
r'{(sot|start_of_tab)}' |
|||
self.lexer.push_state('tablature') |
|||
return token |
|||
|
|||
def t_tablature_EOT(self, token): |
|||
r'{(eot|end_of_tab)}' |
|||
self.lexer.pop_state() |
|||
return token |
|||
|
|||
@staticmethod |
|||
def t_tablature_SPACE(token): |
|||
r'[ \t]+' |
|||
return token |
|||
|
|||
t_tablature_TEXT = r'[^\n]+' |
|||
t_tablature_NEWLINE = r'\n' |
|||
|
|||
def __init__(self): |
|||
self.__class__.lexer = lex.lex(module=self) |
|||
|
|||
# Define a rule so we can track line numbers |
|||
@staticmethod |
|||
def t_NEWLINE(token): |
|||
r'[\n\r]' |
|||
token.lexer.lineno += 1 |
|||
return token |
|||
|
|||
@staticmethod |
|||
def t_COMMENT(token): |
|||
r'\#.*' |
|||
pass |
|||
|
|||
@staticmethod |
|||
def t_WORD(token): |
|||
r'[^{}\n\][\t ]+' |
|||
return token |
|||
|
|||
def t_LBRACKET(self, __token): |
|||
r'\[' |
|||
self.lexer.push_state('chord') |
|||
|
|||
def t_chord_RBRACKET(self, __token): |
|||
r'\]' |
|||
self.lexer.pop_state() |
|||
|
|||
def t_LBRACE(self, token): |
|||
r'{' |
|||
self.lexer.push_state('directive') |
|||
return token |
|||
|
|||
def t_directive_RBRACE(self, token): |
|||
r'}' |
|||
self.lexer.pop_state() |
|||
return token |
|||
|
|||
def t_directiveargument_RBRACE(self, token): |
|||
r'}' |
|||
self.lexer.pop_state() |
|||
self.lexer.pop_state() |
|||
return token |
|||
|
|||
def t_directive_COLON(self, token): |
|||
r':' |
|||
self.lexer.push_state('directiveargument') |
|||
return token |
|||
|
|||
@staticmethod |
|||
def t_error(token): |
|||
"""Manage errors""" |
|||
LOGGER.error("Illegal character '{}'".format(token.value[0])) |
|||
token.lexer.skip(1) |
|||
|
|||
@staticmethod |
|||
def t_chord_error(token): |
|||
"""Manage errors""" |
|||
LOGGER.error("Illegal character '{}' in chord..".format(token.value[0])) |
|||
token.lexer.skip(1) |
|||
|
|||
@staticmethod |
|||
def t_tablature_error(token): |
|||
"""Manage errors""" |
|||
LOGGER.error("Illegal character '{}' in tablature..".format(token.value[0])) |
|||
token.lexer.skip(1) |
|||
|
|||
@staticmethod |
|||
def t_directive_error(token): |
|||
"""Manage errors""" |
|||
LOGGER.error("Illegal character '{}' in directive..".format(token.value[0])) |
|||
token.lexer.skip(1) |
|||
|
|||
def t_directiveargument_error(self, token): |
|||
"""Manage errors""" |
|||
return self.t_directive_error(token) |
@ -0,0 +1,217 @@ |
|||
# -*- coding: utf-8 -*- |
|||
"""ChordPro parser""" |
|||
|
|||
import logging |
|||
import ply.yacc as yacc |
|||
|
|||
from patacrep.errors import SongbookError |
|||
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: |
|||
"""ChordPro parser class""" |
|||
|
|||
start = "song" |
|||
|
|||
def __init__(self, filename=None): |
|||
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 |
|||
""" |
|||
#if isinstance(symbols[1], str): |
|||
if len(symbols) == 2: |
|||
symbols[0] = ast.Song(self.filename) |
|||
else: |
|||
symbols[0] = symbols[2].add(symbols[1]) |
|||
|
|||
@staticmethod |
|||
def p_block(symbols): |
|||
"""block : SPACE block |
|||
| directive NEWLINE |
|||
| line NEWLINE |
|||
| chorus NEWLINE |
|||
| tab NEWLINE |
|||
| bridge NEWLINE |
|||
| NEWLINE |
|||
""" |
|||
if len(symbols) == 3 and isinstance(symbols[1], str): |
|||
symbols[0] = symbols[2] |
|||
elif (symbols[1] is None) or (len(symbols) == 2): |
|||
symbols[0] = None |
|||
else: |
|||
symbols[0] = symbols[1] |
|||
|
|||
@staticmethod |
|||
def p_maybespace(symbols): |
|||
"""maybespace : SPACE |
|||
| empty |
|||
""" |
|||
symbols[0] = None |
|||
|
|||
@staticmethod |
|||
def p_directive(symbols): |
|||
"""directive : LBRACE KEYWORD directive_next RBRACE |
|||
| LBRACE SPACE KEYWORD directive_next RBRACE |
|||
""" |
|||
if len(symbols) == 5: |
|||
symbols[3].keyword = symbols[2] |
|||
symbols[0] = symbols[3] |
|||
else: |
|||
symbols[4].keyword = symbols[3] |
|||
symbols[0] = symbols[4] |
|||
|
|||
@staticmethod |
|||
def p_directive_next(symbols): |
|||
"""directive_next : SPACE COLON TEXT |
|||
| COLON TEXT |
|||
| empty |
|||
""" |
|||
symbols[0] = ast.Directive() |
|||
if len(symbols) == 3: |
|||
symbols[0].argument = symbols[2].strip() |
|||
elif len(symbols) == 4: |
|||
symbols[0].argument = symbols[3].strip() |
|||
|
|||
@staticmethod |
|||
def p_line(symbols): |
|||
"""line : word line_next |
|||
| chord line_next |
|||
""" |
|||
symbols[0] = symbols[2].prepend(symbols[1]) |
|||
|
|||
@staticmethod |
|||
def p_line_next(symbols): |
|||
"""line_next : word line_next |
|||
| space line_next |
|||
| chord line_next |
|||
| empty |
|||
""" |
|||
if len(symbols) == 2: |
|||
symbols[0] = ast.Line() |
|||
else: |
|||
symbols[0] = symbols[2].prepend(symbols[1]) |
|||
|
|||
@staticmethod |
|||
def p_word(symbols): |
|||
"""word : WORD""" |
|||
symbols[0] = ast.Word(symbols[1]) |
|||
|
|||
@staticmethod |
|||
def p_space(symbols): |
|||
"""space : SPACE""" |
|||
symbols[0] = ast.Space() |
|||
|
|||
@staticmethod |
|||
def p_chord(symbols): |
|||
"""chord : CHORD""" |
|||
symbols[0] = ast.Chord(symbols[1]) |
|||
|
|||
@staticmethod |
|||
def p_chorus(symbols): |
|||
"""chorus : SOC maybespace NEWLINE chorus_content EOC maybespace |
|||
""" |
|||
symbols[0] = symbols[4] |
|||
|
|||
@staticmethod |
|||
def p_chorus_content(symbols): |
|||
"""chorus_content : line NEWLINE chorus_content |
|||
| SPACE chorus_content |
|||
| empty |
|||
""" |
|||
if len(symbols) == 2: |
|||
symbols[0] = ast.Chorus() |
|||
elif len(symbols) == 3: |
|||
symbols[0] = symbols[2] |
|||
else: |
|||
symbols[0] = symbols[3].prepend(symbols[1]) |
|||
|
|||
@staticmethod |
|||
def p_bridge(symbols): |
|||
"""bridge : SOB maybespace NEWLINE bridge_content EOB maybespace |
|||
""" |
|||
symbols[0] = symbols[4] |
|||
|
|||
@staticmethod |
|||
def p_bridge_content(symbols): |
|||
"""bridge_content : line NEWLINE bridge_content |
|||
| SPACE bridge_content |
|||
| empty |
|||
""" |
|||
if len(symbols) == 2: |
|||
symbols[0] = ast.Bridge() |
|||
elif len(symbols) == 3: |
|||
symbols[0] = symbols[2] |
|||
else: |
|||
symbols[0] = symbols[3].prepend(symbols[1]) |
|||
|
|||
|
|||
@staticmethod |
|||
def p_tab(symbols): |
|||
"""tab : SOT maybespace NEWLINE tab_content EOT maybespace |
|||
""" |
|||
symbols[0] = symbols[4] |
|||
|
|||
@staticmethod |
|||
def p_tab_content(symbols): |
|||
"""tab_content : NEWLINE tab_content |
|||
| TEXT tab_content |
|||
| SPACE tab_content |
|||
| empty |
|||
""" |
|||
if len(symbols) == 2: |
|||
symbols[0] = ast.Tab() |
|||
else: |
|||
if symbols[1].strip(): |
|||
symbols[2].prepend(symbols[1]) |
|||
symbols[0] = symbols[2] |
|||
|
|||
@staticmethod |
|||
def p_empty(symbols): |
|||
"""empty :""" |
|||
symbols[0] = None |
|||
|
|||
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, |
|||
) |
@ -0,0 +1 @@ |
|||
======== |
@ -0,0 +1 @@ |
|||
A verse line |
@ -0,0 +1,4 @@ |
|||
======== |
|||
{start_of_verse} |
|||
A verse line |
|||
{end_of_verse} |
@ -0,0 +1 @@ |
|||
{title : A directive} |
@ -0,0 +1,2 @@ |
|||
{title: A directive} |
|||
======== |
@ -0,0 +1,4 @@ |
|||
|
|||
|
|||
|
|||
|
@ -0,0 +1 @@ |
|||
======== |
@ -0,0 +1,3 @@ |
|||
{soc} |
|||
A one line chorus |
|||
{eoc} |
@ -0,0 +1,4 @@ |
|||
======== |
|||
{start_of_chorus} |
|||
A one line chorus |
|||
{end_of_chorus} |
@ -0,0 +1,3 @@ |
|||
{sob} |
|||
A one line bridge |
|||
{eob} |
@ -0,0 +1,4 @@ |
|||
======== |
|||
{start_of_bridge} |
|||
A one line bridge |
|||
{end_of_bridge} |
@ -0,0 +1 @@ |
|||
# A comment |
@ -0,0 +1 @@ |
|||
======== |
@ -0,0 +1,3 @@ |
|||
{sot} |
|||
A tab |
|||
{eot} |
@ -0,0 +1,4 @@ |
|||
======== |
|||
{start_of_tab} |
|||
A tab |
|||
{end_of_tab} |
@ -0,0 +1,10 @@ |
|||
|
|||
|
|||
|
|||
# comment |
|||
# comment |
|||
|
|||
|
|||
A lot of new lines |
|||
|
|||
# comment |
@ -0,0 +1,4 @@ |
|||
======== |
|||
{start_of_verse} |
|||
A lot of new lines |
|||
{end_of_verse} |
@ -0,0 +1,15 @@ |
|||
|
|||
|
|||
|
|||
# comment |
|||
# comment |
|||
|
|||
|
|||
A lot of new lines |
|||
|
|||
# comment |
|||
|
|||
{title: and a directive} |
|||
|
|||
# comment |
|||
|
@ -0,0 +1,5 @@ |
|||
{title: and a directive} |
|||
======== |
|||
{start_of_verse} |
|||
A lot of new lines |
|||
{end_of_verse} |
@ -0,0 +1 @@ |
|||
A line[A] with a chord |
@ -0,0 +1,4 @@ |
|||
======== |
|||
{start_of_verse} |
|||
A line[A] with a chord |
|||
{end_of_verse} |
@ -0,0 +1 @@ |
|||
A line ending with a chord[A] |
@ -0,0 +1,4 @@ |
|||
======== |
|||
{start_of_verse} |
|||
A line ending with a chord[A] |
|||
{end_of_verse} |
@ -0,0 +1 @@ |
|||
[A]A line starting with a chord |
@ -0,0 +1,4 @@ |
|||
======== |
|||
{start_of_verse} |
|||
[A]A line starting with a chord |
|||
{end_of_verse} |
@ -0,0 +1,5 @@ |
|||
{sot} |
|||
A table |
|||
wit many # weir [ |
|||
[ symbols |
|||
{eot} |
@ -0,0 +1,6 @@ |
|||
======== |
|||
{start_of_tab} |
|||
A table |
|||
wit many # weir [ |
|||
[ symbols |
|||
{end_of_tab} |
@ -0,0 +1 @@ |
|||
A verse line |
@ -0,0 +1,4 @@ |
|||
======== |
|||
{start_of_verse} |
|||
A verse line |
|||
{end_of_verse} |
@ -0,0 +1 @@ |
|||
{title : A directive} |
@ -0,0 +1,2 @@ |
|||
{title: A directive} |
|||
======== |
@ -0,0 +1,4 @@ |
|||
|
|||
|
|||
|
|||
|
@ -0,0 +1 @@ |
|||
======== |
@ -0,0 +1,3 @@ |
|||
{soc} |
|||
A one line chorus |
|||
{eoc} |
@ -0,0 +1,4 @@ |
|||
======== |
|||
{start_of_chorus} |
|||
A one line chorus |
|||
{end_of_chorus} |
@ -0,0 +1,3 @@ |
|||
{sob} |
|||
A one line bridge |
|||
{eob} |
@ -0,0 +1,4 @@ |
|||
======== |
|||
{start_of_bridge} |
|||
A one line bridge |
|||
{end_of_bridge} |
@ -0,0 +1 @@ |
|||
# A comment |
@ -0,0 +1 @@ |
|||
======== |
@ -0,0 +1,3 @@ |
|||
{sot} |
|||
A tab |
|||
{eot} |
@ -0,0 +1,4 @@ |
|||
======== |
|||
{start_of_tab} |
|||
A tab |
|||
{end_of_tab} |
@ -0,0 +1,10 @@ |
|||
|
|||
|
|||
|
|||
# comment |
|||
# comment |
|||
|
|||
|
|||
A lot of new lines |
|||
|
|||
# comment |
@ -0,0 +1,4 @@ |
|||
======== |
|||
{start_of_verse} |
|||
A lot of new lines |
|||
{end_of_verse} |
@ -0,0 +1,15 @@ |
|||
|
|||
|
|||
|
|||
# comment |
|||
# comment |
|||
|
|||
|
|||
A lot of new lines |
|||
|
|||
# comment |
|||
|
|||
{title: and a directive} |
|||
|
|||
# comment |
|||
|
@ -0,0 +1,5 @@ |
|||
{title: and a directive} |
|||
======== |
|||
{start_of_verse} |
|||
A lot of new lines |
|||
{end_of_verse} |
@ -0,0 +1 @@ |
|||
[A]A line starting with a chord |
@ -0,0 +1 @@ |
|||
"""Test for chordpro parser""" |
@ -0,0 +1,44 @@ |
|||
{language : english} |
|||
{columns : 2} |
|||
{subtitle : Un sous titre} |
|||
{ title : Greensleeves} |
|||
{title : Un autre sous-titre} |
|||
{artist: Traditionnel} |
|||
{cover : traditionnel } |
|||
{album :Angleterre} |
|||
|
|||
{partition : greensleeves.ly} |
|||
|
|||
|
|||
A[Am]las, my love, ye [G]do me wrong |
|||
To [Am]cast me oft dis[E]curteously |
|||
And [Am]I have loved [G]you so long |
|||
De[Am]lighting [E]in your [Am]companie |
|||
|
|||
{start_of_chorus} |
|||
[C]Green[B]sleeves was [G]all my joy |
|||
[Am]Greensleeves was [E]my delight |
|||
[C]Greensleeves was my [G]heart of gold |
|||
And [Am]who but [E]Ladie [Am]Greensleeves |
|||
{end_of_chorus} |
|||
|
|||
I [Am]have been ready [G]at your hand |
|||
To [Am]grant what ever [E]you would crave |
|||
I [Am]have both waged [G]life and land |
|||
Your [Am]love and [E]good will [Am]for to have |
|||
|
|||
I [Am]bought thee kerchers [G]to thy head |
|||
That [Am]were wrought fine and [E]gallantly |
|||
I [Am]kept thee both at [G]boord and bed |
|||
Which [Am]cost my [E]purse well [Am]favouredly |
|||
|
|||
I [Am]bought thee peticotes [G]of the best |
|||
The [Am]cloth so fine as [E]fine might be |
|||
I [Am]gave thee jewels [G]for thy chest |
|||
And [Am]all this [E]cost I [Am]spent on thee |
|||
|
|||
|
|||
Thy [Am]smock of silke, both [G]faire and white |
|||
With [Am]gold embrodered [E]gorgeously |
|||
Thy [Am]peticote of [G]sendall right |
|||
And [Am]this I [E]bought thee [Am]gladly |
@ -0,0 +1,52 @@ |
|||
{title: Greensleeves} |
|||
{title: Un autre sous-titre} |
|||
{title: Un sous titre} |
|||
{by: Traditionnel} |
|||
{album: Angleterre} |
|||
{columns: 2} |
|||
{cov: DIRNAME/traditionnel} |
|||
{language: english} |
|||
======== |
|||
{partition: DIRNAME/greensleeves.ly} |
|||
|
|||
{start_of_verse} |
|||
A[Am]las, my love, ye [G]do me wrong |
|||
To [Am]cast me oft dis[E]curteously |
|||
And [Am]I have loved [G]you so long |
|||
De[Am]lighting [E]in your [Am]companie |
|||
{end_of_verse} |
|||
|
|||
{start_of_chorus} |
|||
[C]Green[B]sleeves was [G]all my joy |
|||
[Am]Greensleeves was [E]my delight |
|||
[C]Greensleeves was my [G]heart of gold |
|||
And [Am]who but [E]Ladie [Am]Greensleeves |
|||
{end_of_chorus} |
|||
|
|||
{start_of_verse} |
|||
I [Am]have been ready [G]at your hand |
|||
To [Am]grant what ever [E]you would crave |
|||
I [Am]have both waged [G]life and land |
|||
Your [Am]love and [E]good will [Am]for to have |
|||
{end_of_verse} |
|||
|
|||
{start_of_verse} |
|||
I [Am]bought thee kerchers [G]to thy head |
|||
That [Am]were wrought fine and [E]gallantly |
|||
I [Am]kept thee both at [G]boord and bed |
|||
Which [Am]cost my [E]purse well [Am]favouredly |
|||
{end_of_verse} |
|||
|
|||
{start_of_verse} |
|||
I [Am]bought thee peticotes [G]of the best |
|||
The [Am]cloth so fine as [E]fine might be |
|||
I [Am]gave thee jewels [G]for thy chest |
|||
And [Am]all this [E]cost I [Am]spent on thee |
|||
{end_of_verse} |
|||
|
|||
{start_of_verse} |
|||
Thy [Am]smock of silke, both [G]faire and white |
|||
With [Am]gold embrodered [E]gorgeously |
|||
Thy [Am]peticote of [G]sendall right |
|||
And [Am]this I [E]bought thee [Am]gladly |
|||
{end_of_verse} |
@ -0,0 +1,20 @@ |
|||
{subtitle: Subtitle3} |
|||
{title: Title} |
|||
{title: Subtitle1} |
|||
{subtitle: Subtitle4} |
|||
{t: Subtitle2} |
|||
{st: Subtitle5} |
|||
{language: french} |
|||
{language: english} |
|||
{by: Author1} |
|||
{artist: Author2} |
|||
{album: Albom} |
|||
{copyright: Copyright} |
|||
{cover: Cover} |
|||
{vcover: VCover} |
|||
{capo: Capo} |
|||
{key: foo: Foo} |
|||
{comment: Comment} |
|||
{guitar_comment: GuitarComment} |
|||
{image: Image} |
|||
{partition: Lilypond} |
@ -0,0 +1,21 @@ |
|||
{title: Title} |
|||
{title: Subtitle1} |
|||
{title: Subtitle2} |
|||
{title: Subtitle3} |
|||
{title: Subtitle4} |
|||
{title: Subtitle5} |
|||
{by: Author1} |
|||
{by: Author2} |
|||
{key: {foo: Foo}} |
|||
{album: Albom} |
|||
{capo: Capo} |
|||
{copyright: Copyright} |
|||
{cov: DIRNAME/Cover} |
|||
{language: english} |
|||
{language: french} |
|||
{vcov: VCover} |
|||
======== |
|||
{comment: Comment} |
|||
{guitar_comment: GuitarComment} |
|||
{image: DIRNAME/Image} |
|||
{partition: DIRNAME/Lilypond} |
@ -0,0 +1,56 @@ |
|||
"""Tests for the chordpro parser.""" |
|||
|
|||
# pylint: disable=too-few-public-methods |
|||
|
|||
import glob |
|||
import os |
|||
import unittest |
|||
|
|||
from patacrep.songs.chordpro import syntax as chordpro |
|||
|
|||
class ParserTxtRenderer(unittest.TestCase): |
|||
"""Test parser, and renderer as a txt file.""" |
|||
|
|||
maxDiff = None |
|||
|
|||
def __init__(self, methodname="runTest", basename=None): |
|||
super().__init__(methodname) |
|||
self.basename = basename |
|||
|
|||
def shortDescription(self): |
|||
return "Parsing file '{}.txt'.".format(self.basename) |
|||
|
|||
def runTest(self): |
|||
"""Test txt output (default, debug output).""" |
|||
# pylint: disable=invalid-name |
|||
|
|||
if self.basename is None: |
|||
return |
|||
with open("{}.sgc".format(self.basename), 'r', encoding='utf8') as sourcefile: |
|||
with open("{}.txt".format(self.basename), 'r', encoding='utf8') as expectfile: |
|||
#print(os.path.basename(sourcefile.name)) |
|||
#with open("{}.txt.diff".format(self.basename), 'w', encoding='utf8') as difffile: |
|||
# difffile.write( |
|||
# str(chordpro.parse_song( |
|||
# sourcefile.read(), |
|||
# os.path.basename(sourcefile.name), |
|||
# )).strip() |
|||
# ) |
|||
# sourcefile.seek(0) |
|||
self.assertMultiLineEqual( |
|||
str(chordpro.parse_song( |
|||
sourcefile.read(), |
|||
os.path.abspath(sourcefile.name), |
|||
)).strip(), |
|||
expectfile.read().strip().replace("DIRNAME", os.path.dirname(self.basename)), |
|||
) |
|||
|
|||
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', |
|||
))): |
|||
tests.addTest(ParserTxtRenderer(basename=txt[:-len('.txt')])) |
|||
return tests |
@ -0,0 +1,39 @@ |
|||
"""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, encoding |
|||
from patacrep.latex import parse_song |
|||
from patacrep.songs import Song |
|||
|
|||
class LatexSong(Song): |
|||
"""LaTeX song parser.""" |
|||
|
|||
def parse(self, __config): |
|||
"""Parse content, and return the dictinory of song data.""" |
|||
with encoding.open_read(self.fullpath, encoding=self.encoding) as song: |
|||
self.data = parse_song(song.read(), self.fullpath) |
|||
self.titles = self.data['@titles'] |
|||
del self.data['@titles'] |
|||
self.languages = self.data['@languages'] |
|||
del self.data['@languages'] |
|||
self.authors = [self.data['by']] |
|||
del self.data['by'] |
|||
|
|||
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, |
|||
} |
@ -1,30 +0,0 @@ |
|||
"""Very simple LaTeX parsing.""" |
|||
|
|||
import os |
|||
|
|||
from patacrep import files |
|||
from patacrep.latex import parsesong |
|||
from patacrep.songs import Song |
|||
|
|||
class TexRenderer(Song): |
|||
"""Renderer for song and intersong files.""" |
|||
|
|||
def parse(self): |
|||
"""Parse song and set metadata.""" |
|||
self.data = parsesong(self.fullpath, self.encoding) |
|||
self.titles = self.data['@titles'] |
|||
self.languages = self.data['@languages'] |
|||
self.authors = self.data['by'] |
|||
|
|||
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']) |
|||
))) |
|||
|
|||
FILE_PLUGINS = { |
|||
'sg': TexRenderer, |
|||
'is': TexRenderer, |
|||
} |
@ -0,0 +1,26 @@ |
|||
"""Tests""" |
|||
|
|||
import doctest |
|||
import os |
|||
import unittest |
|||
|
|||
import patacrep |
|||
|
|||
def suite(): |
|||
"""Return a TestSuite object, to test whole `patacrep` package. |
|||
|
|||
Both unittest and doctest are tested. |
|||
""" |
|||
test_loader = unittest.defaultTestLoader |
|||
return test_loader.discover(os.path.dirname(__file__)) |
|||
|
|||
def load_tests(__loader, tests, __pattern): |
|||
"""Load tests (unittests and doctests).""" |
|||
# Loading doctests |
|||
tests.addTests(doctest.DocTestSuite(patacrep)) |
|||
|
|||
# Unittests are loaded by default |
|||
return tests |
|||
|
|||
if __name__ == "__main__": |
|||
unittest.TextTestRunner().run(suite()) |
Loading…
Reference in new issue