Commit 86bbaabe authored by eckhart's avatar eckhart
Browse files

some refactoring and cleanup

parent b7990c51
......@@ -42,7 +42,7 @@ from DHParser.toolkit import load_if_file, escape_re, md5, sane_parser_name, re,
unrepr, compile_python_object, DHPARSER_PARENTDIR, RX_NEVER_MATCH
from DHParser.transform import TransformationFunc, traverse, remove_brackets, \
reduce_single_child, replace_by_single_child, remove_empty, remove_children, \
remove_tokens, flatten, forbid, assert_content
remove_tokens, flatten, forbid, assert_content, apply_unless, has_parent
from DHParser.versionnumber import __version__
......@@ -95,7 +95,7 @@ except ImportError:
from DHParser import start_logging, suspend_logging, resume_logging, is_filename, load_if_file, \\
Grammar, Compiler, nil_preprocessor, PreprocessorToken, Whitespace, Drop, \\
Lookbehind, Lookahead, Alternative, Pop, Token, Synonym, Interleave, \\
Unordered, Option, NegativeLookbehind, OneOrMore, RegExp, Retrieve, Series, Capture, \\
Option, NegativeLookbehind, OneOrMore, RegExp, Retrieve, Series, Capture, \\
ZeroOrMore, Forward, NegativeLookahead, Required, mixin_comment, compile_source, \\
grammar_changed, last_value, matching_bracket, PreprocessorFunc, is_empty, remove_if, \\
Node, TransformationFunc, TransformationDict, transformation_factory, traverse, \\
......@@ -111,7 +111,7 @@ from DHParser import start_logging, suspend_logging, resume_logging, is_filename
get_config_value, XML_SERIALIZATION, SXPRESSION_SERIALIZATION, \\
COMPACT_SERIALIZATION, JSON_SERIALIZATION, access_thread_locals, access_presets, \\
finalize_presets, ErrorCode, RX_NEVER_MATCH, set_tracer, resume_notices_on, \\
trace_history, has_descendant, neg, has_parent, optional_last_value
trace_history, has_descendant, neg, has_ancestor, optional_last_value
'''
......@@ -513,7 +513,7 @@ class EBNFDirectives:
expression. The closest match is the point of reentry
for after a parsing error has error occurred. Other
than the skip field, this configures resuming after
the failing parser (`parser.Series` or `parser.AllOf`)
the failing parser (`parser.Series` or `parser.Interleave`)
has returned.
drop: A set that may contain the elements `DROP_TOKEN` and
......@@ -1276,7 +1276,9 @@ class EBNFCompiler(Compiler):
else:
if self.anonymous_regexp == RX_NEVER_MATCH:
self.tree.new_error(node, 'Illegal value "%s" for Directive "@ drop"! '
' Should be one of %s.' % (content, str(DROP_VALUES)))
'Should be one of %s or an anonymous parser, where '
'the "@anonymous"-directive must preceed the '
'@drop-directive.' % (content, str(DROP_VALUES)))
else:
self.tree.new_error(
node, 'Illegal value "%s" for Directive "@ drop"! Should be one of '
......
......@@ -100,10 +100,9 @@ cdef class Alternative(NaryParser):
pass
cdef class AllOf(MandatoryNary):
cdef public int num_parsers
cdef class SomeOf(NaryParser):
pass
cdef public object repetitions
cdef public object non_mandatory
cdef public object parsers_set
cdef class FlowParser(UnaryParser):
pass
......
......@@ -76,9 +76,6 @@ __all__ = ('ParserError',
'MandatoryNary',
'Series',
'Alternative',
'AllOf',
'SomeOf',
'Unordered',
'INFINITE',
'Interleave',
'Required',
......@@ -522,6 +519,13 @@ class Parser:
the matching string."""
raise NotImplementedError
def is_optional(self) -> Optional[bool]:
"""Returns `True`, if the parser can never fails, i.e. never yields
`None`, instead of a node. Returns `False`, if the parser can fail.
Returns `None` if it is not known whether the parser can fail.
"""
return None
def set_proxy(self, proxy: Optional[ParseFunc]):
"""Sets a proxy that replaces the _parse()-method. The original
parse-method is copied to the `proxied`-filed of the Parser object and
......@@ -1798,13 +1802,16 @@ class Option(UnaryParser):
def __init__(self, parser: Parser) -> None:
super(Option, self).__init__(parser)
# assert isinstance(parser, Parser)
assert not isinstance(parser, Option), \
assert not parser.is_optional(), \
"Redundant nesting of options: %s(%s)" % (self.ptype, parser.pname)
def _parse(self, text: StringView) -> Tuple[Optional[Node], StringView]:
node, text = self.parser(text)
return self._return_value(node), text
def is_optional(self) -> Optional[bool]:
return True
def __repr__(self):
return '[' + (self.parser.repr[1:-1] if isinstance(self.parser, Alternative)
and not self.parser.pname else self.parser.repr) + ']'
......@@ -1879,7 +1886,7 @@ class OneOrMore(UnaryParser):
def __init__(self, parser: Parser) -> None:
super(OneOrMore, self).__init__(parser)
assert not isinstance(parser, Option), \
assert not parser.is_optional(), \
"Use ZeroOrMore instead of nesting OneOrMore and Option: " \
"%s(%s)" % (self.ptype, parser.pname)
......@@ -2158,8 +2165,9 @@ class Alternative(NaryParser):
assert len(parsers) >= 1
assert len(set(parsers)) == len(parsers)
# only the last alternative may be optional. Could this be checked at compile time?
assert all(not isinstance(p, Option) for p in parsers[:-1]), \
"Parser-specification Error: only the last alternative may be optional!"
assert all(not p.is_optional() for p in parsers[:-1]), \
"Parser-specification Error: Only the last alternative may be optional!" \
"Otherwise alternatives after the first optional alternative will never be parsed."
super(Alternative, self).__init__(*parsers)
def _parse(self, text: StringView) -> Tuple[Optional[Node], StringView]:
......@@ -2202,168 +2210,35 @@ class Alternative(NaryParser):
return self
class AllOf(MandatoryNary):
"""
DEPRECATED, will be removed soon, use Interleave instead!!!
INFINITE = 2**30
Matches if all elements of a list of parsers match. Each parser must
match exactly once. Other than in a sequence, the order in which
the parsers match is arbitrary, however.
Example::
class Interleave(MandatoryNary):
"""Parse elements in arbitrary order.
>>> prefixes = AllOf(TKN("A"), TKN("B"))
Examples::
>>> prefixes = Interleave(TKN("A"), TKN("B"))
>>> Grammar(prefixes)('A B').content
'A B'
>>> Grammar(prefixes)('B A').content
'B A'
Note: The semantics of the mandatory-parameter differs for `AllOf` from
that of `Series`: Rather than the position of the sub-parser starting
from which all following parsers cause the Series-parser to raise an
Error instead of returning a non-match, an error is raised if and only
if the parsers up to (but not including the one at) the mandatory-position
have already been exhausted, i.e. have already captured content for the
AllOf-parser. Otherwise no error is raised, but just a non-match is
returned.
EBNF-Notation: ``<... ...>`` (sequence of parsers enclosed by angular brackets)
EBNF-Example: ``set = <letter letter_or_digit>``
"""
def __init__(self, *parsers: Parser,
mandatory: int = NO_MANDATORY,
err_msgs: MessagesType = [],
skip: ResumeList = []) -> None:
assert all(not isinstance(p, Option) for p in parsers[:-1]), \
"Only the last parser from unordered sequence may be optional!"
if len(parsers) == 1 and isinstance(parsers[0], Series):
parsers = parsers[0].parsers
if self.mandatory == NO_MANDATORY:
self.mandatory = parsers[0].mandatory
assert len(parsers) > 1, "AllOf requires at least two sub-parsers."
super(AllOf, self).__init__(*parsers, mandatory=mandatory, err_msgs=err_msgs, skip=skip)
def _parse(self, text: StringView) -> Tuple[Optional[Node], StringView]:
results = () # type: Tuple[Node, ...]
text_ = text # type: StringView
parsers = list(self.parsers) # type: List[Parser]
error = None # type: Optional[Error]
while parsers:
for i, parser in enumerate(parsers):
node, text__ = parser(text_)
if node is not None:
if node._result or not node.tag_name.startswith(':'):
# drop anonymous empty nodes
results += (node,)
text_ = text__
del parsers[i]
break
else:
for i, p in enumerate(self.parsers):
if p in parsers and i < self.mandatory:
return None, text
reloc = self.get_reentry_point(text_)
expected = '< ' + ' '.join([parser.repr for parser in parsers]) + ' >'
error, err_node, text_ = self.mandatory_violation(text_, False, expected, reloc)
results += (err_node,)
if reloc < 0:
parsers = []
assert len(results) <= len(self.parsers) \
or len(self.parsers) >= len([p for p in results if p.tag_name != ZOMBIE_TAG])
nd = self._return_values(results) # type: Node
if error and reloc < 0:
raise ParserError(nd.with_pos(self.grammar.document_length__ - len(text)),
text, error, first_throw=True)
return nd, text_
def __repr__(self):
return '< ' + ' '.join(parser.repr for parser in self.parsers) + ' >'
class SomeOf(NaryParser):
"""
DEPRECATED, will be removed soon, use Interleave instead!!!
Matches if at least one element of a list of parsers match. No parser
can match more than once.
Example::
>>> prefixes = SomeOf(TKN("A"), TKN("B"))
>>> prefixes = Interleave(TKN("A"), TKN("B"), repetitions=((0, 1), (0, 1)))
>>> Grammar(prefixes)('A B').content
'A B'
>>> Grammar(prefixes)('B A').content
'B A'
>>> Grammar(prefixes)('B').content
'B'
EBNF-Notation: ``<... ...>`` (sequence of parsers enclosed by angular brackets)
EBNF-Example: ``set = <letter letter_or_digit>``
"""
def __init__(self, *parsers: Parser) -> None:
if len(parsers) == 1 and isinstance(parsers[0], Alternative):
parsers = parsers[0].parsers
assert len(parsers) > 1, "SomeOf requires at least two sub-parsers."
assert all(not isinstance(p, Option) for p in parsers[:-1]), \
"Only the last parser from unordered alternative may be optional!"
super(SomeOf, self).__init__(*parsers)
def _parse(self, text: StringView) -> Tuple[Optional[Node], StringView]:
results = () # type: Tuple[Node, ...]
text_ = text # type: StringView
parsers = list(self.parsers) # type: List[Parser]
while parsers:
for i, parser in enumerate(parsers):
node, text__ = parser(text_)
if node is not None:
if node._result or not node.tag_name.startswith(':'):
# drop anonymous empty nodes
results += (node,)
text_ = text__
del parsers[i]
break
else:
parsers = []
assert len(results) <= len(self.parsers)
if results:
return self._return_values(results), text_
else:
return None, text
def __repr__(self):
return '< ' + ' | '.join(parser.repr for parser in self.parsers) + ' >'
def Unordered(parser: NaryParser) -> NaryParser:
"""
Returns an AllOf- or SomeOf-parser depending on whether `parser`
is a Series (AllOf) or an Alternative (SomeOf).
"""
if isinstance(parser, Series):
return AllOf(parser)
elif isinstance(parser, Alternative):
return SomeOf(parser)
else:
raise AssertionError("Unordered can take only Series or Alternative as parser.")
INFINITE = 2**30
class Interleave(MandatoryNary):
"""Parse elements in arbitrary order."""
def __init__(self, *parsers: Parser,
mandatory: int = NO_MANDATORY,
err_msgs: MessagesType = [],
skip: ResumeList = [],
repetitions: Sequence[Tuple[int, int]] = ()) -> None:
assert len(set(parsers)) == len(parsers)
assert all(not isinstance(parser, Option)
assert all(not parser.is_optional()
and not isinstance(parser, FlowParser) for parser in parsers)
super(Interleave, self).__init__(
*parsers, mandatory=mandatory, err_msgs=err_msgs, skip=skip)
......@@ -2424,6 +2299,9 @@ class Interleave(MandatoryNary):
text, error, first_throw=True)
return nd, text_
def is_optional(self) -> Optional[bool]:
return all(r[0] == 0 for r in self.repetitions)
def __repr__(self):
return ' ° '.join(parser.repr for parser in self.parsers)
......
......@@ -33,7 +33,7 @@ cpdef is_empty(context: List[Node])
# cpdef not_one_of(context: List[Node], tag_name_set: AbstractSet[str])
# cpdef matches_re(context: List[Node], patterns: AbstractSet[str])
# cpdef has_content(context: List[Node], regexp: str)
# cpdef has_parent(context: List[Node], tag_name_set: AbstractSet[str])
# cpdef has_ancestor(context: List[Node], tag_name_set: AbstractSet[str])
cpdef _replace_by(node: Node, child: Node)
cpdef _reduce_child(node: Node, child: Node)
cpdef replace_by_single_child(context: List[Node])
......
......@@ -90,8 +90,10 @@ __all__ = ('TransformationDict',
'has_attr',
'attr_equals',
'has_content',
'has_ancestor',
'has_parent',
'has_descendant',
'has_child',
'has_sibling',
'lstrip',
'rstrip',
......@@ -585,33 +587,47 @@ def has_content(context: List[Node], regexp: str) -> bool:
@transformation_factory(collections.abc.Set)
def has_parent(context: List[Node], tag_name_set: AbstractSet[str], ancestry: int = 1) -> bool:
def has_ancestor(context: List[Node], tag_name_set: AbstractSet[str], ancestry: int = 1) -> bool:
"""
Checks whether a node with one of the given tag names appears somewhere
in the context before the last node in the context.
:param ancestry: determines how deep `has_parent` should dive into
:param ancestry: determines how deep `has_ancestor` should dive into
the ancestry. "1" means only the immediate parents wil be considered,
"2" means also the grandparents, ans so on.
"""
assert ancestry > 0
for i in range(2, max(ancestry + 2, len(context) + 1)):
for i in range(2, min(ancestry + 2, len(context) + 1)):
if context[-i].tag_name in tag_name_set:
return True
return False
@transformation_factory(collections.abc.Set)
def has_parent(context: List[Node], tag_name_set: AbstractSet[str]) -> bool:
"""Checks whether the immediate predecessor in the context has one of the
given tags."""
return has_ancestor(context, tag_name_set, 1)
@transformation_factory(collections.abc.Set)
def has_descendant(context: List[Node], tag_name_set: AbstractSet[str],
stop_level: int = 1) -> bool:
assert stop_level > 0
generations: int = 1) -> bool:
assert generations > 0
for child in context[-1].children:
if child.tag_name in tag_name_set:
return True
if stop_level > 1 and has_descendant(context + [child], tag_name_set, stop_level - 1):
if generations > 1 and has_descendant(context + [child], tag_name_set, generations - 1):
return True
return False
@transformation_factory(collections.abc.Set)
def has_child(context: List[Node], tag_name_set: AbstractSet[str]) -> bool:
"""Checks whether at least one child (i.e. immediate descendant) has one of
the given tags."""
return has_descendant(context, tag_name_set, 1)
@transformation_factory(collections.abc.Set)
def has_sibling(context: List[Node], tag_name_set: AbstractSet[str]):
if len(context) >= 2:
......
......@@ -45,7 +45,7 @@ from DHParser import start_logging, suspend_logging, resume_logging, is_filename
get_config_value, XML_SERIALIZATION, SXPRESSION_SERIALIZATION, \
COMPACT_SERIALIZATION, JSON_SERIALIZATION, access_thread_locals, access_presets, \
finalize_presets, ErrorCode, RX_NEVER_MATCH, set_tracer, resume_notices_on, \
trace_history, has_descendant, neg, has_parent, optional_last_value
trace_history, has_descendant, neg, has_ancestor, optional_last_value
#######################################################################
......
......@@ -39,7 +39,7 @@ from DHParser import start_logging, suspend_logging, resume_logging, is_filename
chain, get_config_value, XML_SERIALIZATION, SXPRESSION_SERIALIZATION, \
COMPACT_SERIALIZATION, JSON_SERIALIZATION, access_thread_locals, access_presets, \
finalize_presets, ErrorCode, RX_NEVER_MATCH, set_tracer, resume_notices_on, \
trace_history, has_descendant, neg, has_parent
trace_history, has_descendant, neg, has_ancestor
#######################################################################
......
......@@ -45,7 +45,7 @@ from DHParser import start_logging, suspend_logging, resume_logging, is_filename
get_config_value, XML_SERIALIZATION, SXPRESSION_SERIALIZATION, \
COMPACT_SERIALIZATION, JSON_SERIALIZATION, access_thread_locals, access_presets, \
finalize_presets, ErrorCode, RX_NEVER_MATCH, set_tracer, resume_notices_on, \
trace_history, has_descendant, neg, has_parent, optional_last_value
trace_history, has_descendant, neg, has_ancestor, optional_last_value
#######################################################################
......
......@@ -3,8 +3,9 @@
@ comment = /#.*(?:\n|$)/ # comments start with '#' and eat all chars up to and including '\n'
@ whitespace = /\s*/ # whitespace includes linefeed
@ literalws = right # trailing whitespace of literals will be ignored tacitly
@ drop = whitespace, DEF, OR, AND, ENDL, EOF # do not include these even in the concrete syntax tree
@ anonymous = pure_elem
@ anonymous = pure_elem, EOF
@ drop = whitespace, EOF # do not include these even in the concrete syntax tree
#: top-level
......
......@@ -45,7 +45,7 @@ from DHParser import start_logging, suspend_logging, resume_logging, is_filename
get_config_value, XML_SERIALIZATION, SXPRESSION_SERIALIZATION, \
COMPACT_SERIALIZATION, JSON_SERIALIZATION, access_thread_locals, access_presets, \
finalize_presets, ErrorCode, RX_NEVER_MATCH, set_tracer, resume_notices_on, \
trace_history, has_descendant, neg, has_parent, optional_last_value
trace_history, has_descendant, neg, has_ancestor, optional_last_value
#######################################################################
......@@ -73,8 +73,8 @@ class FlexibleEBNFGrammar(Grammar):
"""
element = Forward()
expression = Forward()
source_hash__ = "4fea3c06a47697f754ff7f60def16f2f"
anonymous__ = re.compile('pure_elem$')
source_hash__ = "ee9bb06eb025ac0cabad051be9e81457"
anonymous__ = re.compile('pure_elem$|EOF$')
static_analysis_pending__ = [] # type: List[bool]
parser_initialization__ = ["upon instantiation"]
COMMENT__ = r'#.*(?:\n|$)'
......@@ -87,7 +87,7 @@ class FlexibleEBNFGrammar(Grammar):
AND = Capture(Alternative(Token(","), Token("")))
OR = Capture(Token("|"))
DEF = Capture(Alternative(Token("="), Token(":="), Token("::=")))
EOF = Series(NegativeLookahead(RegExp('.')), Option(Pop(DEF, match_func=optional_last_value)), Option(Pop(OR, match_func=optional_last_value)), Option(Pop(AND, match_func=optional_last_value)), Option(Pop(ENDL, match_func=optional_last_value)))
EOF = Drop(Series(Drop(NegativeLookahead(RegExp('.'))), Drop(Option(Drop(Pop(DEF, match_func=optional_last_value)))), Drop(Option(Drop(Pop(OR, match_func=optional_last_value)))), Drop(Option(Drop(Pop(AND, match_func=optional_last_value)))), Drop(Option(Drop(Pop(ENDL, match_func=optional_last_value))))))
whitespace = Series(RegExp('~'), dwsp__)
regexp = Series(RegExp('/(?:(?<!\\\\)\\\\(?:/)|[^/])*?/'), dwsp__)
plaintext = Series(RegExp('`(?:(?<!\\\\)\\\\`|[^`])*?`'), dwsp__)
......
......@@ -45,7 +45,7 @@ from DHParser import start_logging, suspend_logging, resume_logging, is_filename
get_config_value, XML_SERIALIZATION, SXPRESSION_SERIALIZATION, \
COMPACT_SERIALIZATION, JSON_SERIALIZATION, access_thread_locals, access_presets, \
finalize_presets, ErrorCode, RX_NEVER_MATCH, set_tracer, resume_notices_on, \
trace_history, has_descendant, neg, has_parent, optional_last_value
trace_history, has_descendant, neg, has_ancestor, optional_last_value
#######################################################################
......
......@@ -39,7 +39,7 @@ from DHParser import start_logging, suspend_logging, resume_logging, is_filename
chain, get_config_value, XML_SERIALIZATION, SXPRESSION_SERIALIZATION, \
COMPACT_SERIALIZATION, JSON_SERIALIZATION, access_thread_locals, access_presets, \
finalize_presets, ErrorCode, RX_NEVER_MATCH, set_tracer, resume_notices_on, \
trace_history, has_descendant, neg, has_parent
trace_history, has_descendant, neg, has_ancestor
#######################################################################
......
......@@ -89,7 +89,7 @@ from DHParser import start_logging, suspend_logging, resume_logging, is_filename
get_config_value, XML_SERIALIZATION, SXPRESSION_SERIALIZATION, \\
COMPACT_SERIALIZATION, JSON_SERIALIZATION, access_thread_locals, access_presets, \\
finalize_presets, ErrorCode, RX_NEVER_MATCH, set_tracer, resume_notices_on, \\
trace_history, has_descendant, neg, has_parent, optional_last_value
trace_history, has_descendant, neg, has_ancestor, optional_last_value
'''
......
......@@ -39,7 +39,7 @@ from DHParser import start_logging, suspend_logging, resume_logging, is_filename
get_config_value, XML_SERIALIZATION, SXPRESSION_SERIALIZATION, \
COMPACT_SERIALIZATION, JSON_SERIALIZATION, access_thread_locals, access_presets, \
finalize_presets, ErrorCode, RX_NEVER_MATCH, set_tracer, resume_notices_on, \
trace_history, has_descendant, neg, has_parent
trace_history, has_descendant, neg, has_ancestor
#######################################################################
......
......@@ -39,7 +39,7 @@ from DHParser import start_logging, suspend_logging, resume_logging, is_filename
get_config_value, XML_SERIALIZATION, SXPRESSION_SERIALIZATION, \
COMPACT_SERIALIZATION, JSON_SERIALIZATION, access_thread_locals, access_presets, \
finalize_presets, ErrorCode, RX_NEVER_MATCH, set_tracer, resume_notices_on, \
trace_history, has_descendant, neg, has_parent
trace_history, has_descendant, neg, has_ancestor
#######################################################################
......
......@@ -820,6 +820,8 @@ class TestInterleaveResume:
assert not st.errors
lang = """document = ({ `A` }) ° `B`\n"""
# code, errors, ast = compile_ebnf(lang, 'InterleaveTest', True)
# print(ast.as_sxpr())
gr = grammar_provider(lang)()
mini_suite(gr)
st = gr('AABAA')
......
......@@ -31,7 +31,7 @@ from DHParser.toolkit import compile_python_object, re
from DHParser.log import is_logging, log_ST, log_parsing_history
from DHParser.error import Error, is_error
from DHParser.parse import ParserError, Parser, Grammar, Forward, TKN, ZeroOrMore, RE, \
RegExp, Lookbehind, NegativeLookahead, OneOrMore, Series, Alternative, AllOf, SomeOf, \
RegExp, Lookbehind, NegativeLookahead, OneOrMore, Series, Alternative, \
Interleave, UnknownParserError, MetaParser, Token, EMPTY_NODE, Capture, Drop, Whitespace, \
GrammarError
from DHParser import compile_source
......@@ -485,20 +485,20 @@ class TestSeries:
class TestAllOfSomeOf:
def test_allOf_order(self):
"""Test that parsers of an AllOf-List can match in arbitrary order."""
prefixes = AllOf(TKN("A"), TKN("B"))
prefixes = Interleave(TKN("A"), TKN("B"))
assert Grammar(prefixes)('A B').content == 'A B'
assert Grammar(prefixes)('B A').content == 'B A'
def test_allOf_completeness(self):
"""Test that an error is raised if not all parsers of an AllOf-List
match."""
prefixes = AllOf(TKN("A"), TKN("B"))
prefixes = Interleave(TKN("A"), TKN("B"))
assert Grammar(prefixes)('B').error_flag
def test_allOf_redundance(self):
"""Test that one and the same parser may be listed several times
and must be matched several times accordingly."""
prefixes = AllOf(TKN("A"), TKN("B"), TKN("A"))
prefixes = Interleave(TKN("A"), TKN("B"), TKN("A"))
assert Grammar(prefixes)('A A B').content == 'A A B'
assert Grammar(prefixes)('A B A').content == 'A B A'
assert Grammar(prefixes)('B A A').content == 'B A A'
......@@ -506,17 +506,21 @@ class TestAllOfSomeOf:
def test_someOf_order(self):
"""Test that parsers of an AllOf-List can match in arbitrary order."""
prefixes = SomeOf(TKN("A"), TKN("B"))
prefixes = Interleave(TKN("A"), TKN("B"))
assert Grammar(prefixes)('A B').content == 'A B'
assert Grammar(prefixes)('B A').content == 'B A'
prefixes = SomeOf(TKN("B"), TKN("A"))
st = Grammar(prefixes)('B')
assert st.error_flag
prefixes = Interleave(TKN("B"), TKN("A"), repetitions=((0, 1), (0, 1)))
assert Grammar(prefixes)('A B').content == 'A B'
assert Grammar(prefixes)('B').content == 'B'
st = Grammar(prefixes)('B')
assert not st.error_flag
assert st.content == 'B'
def test_someOf_redundance(self):
"""Test that one and the same parser may be listed several times
and must be matched several times accordingly."""
prefixes = SomeOf(TKN("A"), TKN("B"), TKN("A"))
prefixes = Interleave(TKN("A"), TKN("B"), TKN("A"))
assert Grammar(prefixes)('A A B').content == 'A A B'
assert Grammar(prefixes)('A B A').content == 'A B A'
assert Grammar(prefixes)('B A A').content == 'B A A'
......
......@@ -30,7 +30,7 @@ from DHParser.syntaxtree import Node, parse_sxpr, parse_xml, PLACEHOLDER, \
tree_sanity_check, flatten_sxpr
from DHParser.transform import traverse, reduce_single_child, remove_whitespace, move_adjacent, \
traverse_locally, collapse, collapse_children_if, lstrip, rstrip, remove_content, \
remove_tokens, transformation_factory, has_parent, contains_only_whitespace, \
remove_tokens, transformation_factory, has_ancestor, has_parent, contains_only_whitespace, \
is_insignificant_whitespace, merge_adjacent, is_one_of, swap_attributes, delimit_children, \
positions_of, insert, node_maker, apply_if, change_tag_name, add_attributes
from typing import AbstractSet, List, Sequence, Tuple
......@@ -168,10 +168,15 @@ class TestConditionalTransformations:
"""Tests conditional transformations."""
def test_has_parent(self):
context = [Node('A', 'alpha'),
context = [Node('C', 'alpha'),
Node('B', 'beta'),
Node('C', 'gamma')]
assert has_parent(context, {'A'}, 2)
Node('A', 'gamma')]
assert not has_ancestor(context, {'A'})
assert has_ancestor(context, {'B'})
assert not has_ancestor(context, {'C'})
assert has_ancestor(context, {'C'}, 2)
assert not has_parent(context, {'A'})
assert has_parent(context, {'B'})
assert not has_parent(context, {'C'})
......