Skip to content

Commit

Permalink
Allow rules with the same name
Browse files Browse the repository at this point in the history
Even at the same level!
The rules with the same names are collated into a list.

Resolves #9
  • Loading branch information
yanivmo committed Jun 6, 2017
1 parent 1b5d0c9 commit c23d19f
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 28 deletions.
3 changes: 0 additions & 3 deletions src/ruler/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,10 @@
Optional, \
OneOf

from .base_rules import TokenRedefinitionError

__all__ = [
'Grammar',
'Rule',
'RegexRule',
'Optional',
'OneOf',
'TokenRedefinitionError',
]
35 changes: 19 additions & 16 deletions src/ruler/base_rules.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import collections


class BaseRule(object):
"""
Expand Down Expand Up @@ -65,18 +67,25 @@ def register_named_subrules(self):
self._named_rules = {}

for rule in self._rules:
grandchild_rules = rule.register_named_subrules()
# If a rule has a name, the rule itself will be added as a named sub-rule;
# if doesn't have a name, the named sub-rules of the rule will be added as
# direct named sub-rules, skipping one level
subrules = rule.register_named_subrules()
if rule.name:
if rule.name in self._named_rules:
raise TokenRedefinitionError(self, rule.name)
else:
self._named_rules[rule.name] = rule
else:
redefined_keys = set(self._named_rules) & set(grandchild_rules)
if redefined_keys:
raise TokenRedefinitionError(self, redefined_keys)
subrules = {rule.name: rule}

for name, subrule in subrules.items():
existing = self._named_rules.get(name)
# If more than one named sub-rule with the same name exist, the name
# will actually reference a list of rules
if existing:
if isinstance(existing, collections.MutableSequence):
existing.append(subrule)
else:
self._named_rules[name] = [existing, subrule]
else:
self._named_rules.update(grandchild_rules)
self._named_rules[name] = subrule

return self._named_rules

def clone(self):
Expand Down Expand Up @@ -126,12 +135,6 @@ def long_description(self):
)


class TokenRedefinitionError(Exception):
"""Raised if a grammar contains multiple tokens with the same name"""
def __init__(self, rule, token_name):
super(TokenRedefinitionError, self).__init__('"{}" in {}'.format(token_name, repr(rule)))


class RuleNamingError(Exception):
"""Raised on an attempt to set a name to an already named rule"""
def __init__(self, name):
Expand Down
48 changes: 39 additions & 9 deletions tests/test_ruler.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from pytest import raises

from ruler import Rule, Optional, OneOf, Grammar, RegexRule, TokenRedefinitionError
from ruler import Rule, Optional, OneOf, Grammar, RegexRule
from ruler.base_rules import BaseRule, BaseCompoundRule, RuleNamingError
from ruler.rules import CompoundRule

Expand Down Expand Up @@ -185,11 +185,22 @@ def test_flattening(self):

class TestTokenErrors:
def test_sibling_redefinition(self):
with raises(TokenRedefinitionError):
Rule(Rule.with_name('A')('a'), Rule.with_name('A')('b'))
r = Rule(Rule.with_name('x')('a'), Rule.with_name('x')('b'))

with raises(TokenRedefinitionError):
OneOf(Rule.with_name('A')('a'), Rule.with_name('A')('b'))
assert r.match('ab')
assert r.x[0].matched == 'a'
assert r.x[1].matched == 'b'

def test_oneof_sibling_redefinition(self):
r = OneOf(Rule.with_name('x')('a'), Rule.with_name('x')('b'))

assert r.match('a')
assert r.x[0].matched == 'a'
assert r.x[1].matched is None

assert r.match('b')
assert r.x[0].matched is None
assert r.x[1].matched == 'b'

def test_child_redefinition(self):
r = Rule.with_name('x')('a',
Expand All @@ -205,8 +216,7 @@ def test_child_redefinition(self):
assert r.x.x.matched == 'c'

def test_flattened(self):
with raises(TokenRedefinitionError):
Rule(
r = Rule(
Rule.with_name('xx')('a'),
Rule(
Rule.with_name('yy')('b'),
Expand All @@ -215,6 +225,16 @@ def test_flattened(self):
)
)

assert r.match('abc')
assert r.xx[0].matched == 'a'
assert r.yy.matched == 'b'
assert r.xx[1].matched == 'c'

assert r.match('ab')
assert r.xx[0].matched == 'a'
assert r.yy.matched == 'b'
assert r.xx[1].matched is None


class TestAutomaticRuleNaming:
def test_example(self):
Expand All @@ -230,12 +250,13 @@ def test_example(self):
"""

class MorningGrammar(Grammar):
who = OneOf('John', 'Peter', 'Ann')
person = OneOf('John', 'Peter', 'Ann', 'Paul', 'Rachel')
who = Rule(person, Optional(', ', person), Optional(' and ', person))
juice = Rule('juice')
milk = Optional(' with milk')
tea = Rule('tea', milk)
what = OneOf(juice, tea)
grammar = Rule(who, ' likes to drink ', what, '\.')
grammar = Rule(who, ' like', Optional('s'), ' to drink ', what, '\.')

r = MorningGrammar.create()

Expand All @@ -260,6 +281,15 @@ class MorningGrammar(Grammar):
assert r.what.tea.matched
assert r.what.tea.milk.matched is None

assert r.match('Peter, Rachel and Ann like to drink juice.')
assert r.who.matched == 'Peter, Rachel and Ann'
assert r.who.person[0].matched == 'Peter'
assert r.who.person[1].matched == 'Rachel'
assert r.who.person[2].matched == 'Ann'
assert r.what.matched == 'juice'
assert r.what.juice.matched is not None
assert r.what.tea.matched is None

assert not r.match('Peter likes to drink coffee.')
assert r.error.position == 21

Expand Down

0 comments on commit c23d19f

Please sign in to comment.