Skip to content

Commit

Permalink
refs #57 Support for user meta-data + tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
igordejanovic committed Aug 29, 2018
1 parent fc236c6 commit 3aca5dd
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 25 deletions.
83 changes: 58 additions & 25 deletions parglare/grammar.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ class Terminal(GrammarSymbol):
at the same place and implicit disambiguation doesn't resolve.
keyword(bool): `True` if this Terminal represents keyword. `False` by
default.
user_meta(dict): User meta-data.
recognizer(callable): Called with input list of objects and position in the
stream. Should return a sublist of recognized objects. The sublist
Expand All @@ -147,6 +148,7 @@ def __init__(self, name, recognizer=None, location=None,
self.prefer = False
self.dynamic = False
self.keyword = False
self.user_meta = None
super(Terminal, self).__init__(name, location, imported_with)

@property
Expand All @@ -163,6 +165,18 @@ def recognizer(self, value):
value._pg_context = False
self._recognizer = value

def add_meta_data(self, name, value):
if self.user_meta is None:
self.user_meta = {}
self.user_meta[name] = value

def __getattr__(self, name):
if self.user_meta is not None:
attr = self.user_meta.get(name)
if attr:
return attr
raise AttributeError


class Reference(object):
"""
Expand Down Expand Up @@ -306,14 +320,15 @@ class Production(object):
Only makes sense for GLR parser.
nopse (bool): Disable prefer_shifts_over_empty strategy for this
production. Only makes sense for GLR parser.
user_meta(dict): User meta-data.
prod_id (int): Ordinal number of the production.
prod_symbol_id (int): A zero-based ordinal of alternative choice for this
production grammar symbol.
"""

def __init__(self, symbol, rhs, assignments=None, assoc=ASSOC_NONE,
prior=DEFAULT_PRIORITY, dynamic=False, nops=False,
nopse=False):
nopse=False, user_meta=None):
"""
Args:
symbol (GrammarSymbol): A grammar symbol on the LHS of the production.
Expand All @@ -332,6 +347,7 @@ def __init__(self, symbol, rhs, assignments=None, assoc=ASSOC_NONE,
self.dynamic = dynamic
self.nops = nops
self.nopse = nopse
self.user_meta = user_meta

def __str__(self):
if hasattr(self, 'prod_id'):
Expand All @@ -343,6 +359,13 @@ def __str__(self):
def __repr__(self):
return 'Production({})'.format(str(self.symbol))

def __getattr__(self, name):
if self.user_meta is not None:
attr = self.user_meta.get(name)
if attr:
return attr
raise AttributeError


class ProductionRHS(list):
def __getitem__(self, idx):
Expand Down Expand Up @@ -1567,25 +1590,27 @@ def act_production_rule(context, nodes):
prods = []
attrs = {}
for prod in rhs_prods:
assignments, disrules = prod
assignments, meta_datas = prod
# Here we know the indexes of assignments
for idx, a in enumerate(assignments):
if a.name:
a.index = idx
gsymbols = (a.symbol for a in assignments)
assoc = disrules.get('assoc', ASSOC_NONE)
prior = disrules.get('priority', DEFAULT_PRIORITY)
dynamic = disrules.get('dynamic', False)
nops = disrules.get('nops', False)
nopse = disrules.get('nopse', False)
assoc = meta_datas.get('assoc', ASSOC_NONE)
prior = meta_datas.get('priority', DEFAULT_PRIORITY)
dynamic = meta_datas.get('dynamic', False)
nops = meta_datas.get('nops', False)
nopse = meta_datas.get('nopse', False)
user_meta = meta_datas.get('user_meta')
prods.append(Production(symbol,
ProductionRHS(gsymbols),
assignments=assignments,
assoc=assoc,
prior=prior,
dynamic=dynamic,
nops=nops,
nopse=nopse))
nopse=nopse,
user_meta=user_meta))

for a in assignments:
if a.name:
Expand Down Expand Up @@ -1646,30 +1671,38 @@ def __repr__(self):

def act_production(_, nodes):
assignments = nodes[0]
disrules = {}
meta_datas = {}
if len(nodes) > 1:
rules = nodes[2]
for rule in rules:
if rule in ['left', 'reduce']:
disrules['assoc'] = ASSOC_LEFT
elif rule in ['right', 'shift']:
disrules['assoc'] = ASSOC_RIGHT
elif rule == 'dynamic':
disrules['dynamic'] = True
elif rule == 'nops':
disrules['nops'] = True
elif rule == 'nopse':
disrules['nopse'] = True
elif type(rule) is int:
disrules['priority'] = rule

return (assignments, disrules)
for meta_data in nodes[2]:
if meta_data in ['left', 'reduce']:
meta_datas['assoc'] = ASSOC_LEFT
elif meta_data in ['right', 'shift']:
meta_datas['assoc'] = ASSOC_RIGHT
elif meta_data == 'dynamic':
meta_datas['dynamic'] = True
elif meta_data == 'nops':
meta_datas['nops'] = True
elif meta_data == 'nopse':
meta_datas['nopse'] = True
elif type(meta_data) is int:
meta_datas['priority'] = meta_data
else:
# User meta-data
assert type(meta_data) is list
name, _, value = meta_data
meta_datas.setdefault('user_meta', {})[name] = value

return (assignments, meta_datas)


def _set_term_props(term, props):
for t in props:
if type(t) is int:
term.prior = t
elif type(t) is list:
# User meta-data
name, _, value = t
term.add_meta_data(name, value)
elif t == 'finish':
term.finish = True
elif t == 'nofinish':
Expand Down
67 changes: 67 additions & 0 deletions tests/func/test_meta_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""
Allow arbitrary user meta-data.
See issue: https://github.com/igordejanovic/parglare/issues/57
"""
import pytest
from parglare import Grammar, ParseError
from parglare.grammar import ASSOC_LEFT


def test_production_meta_data():

grammar_str = r'''
MyRule: 'a' {left, 1, dynamic, nops, label:'My Label'};
'''

grammar = Grammar.from_string(grammar_str)
my_rule = grammar.get_nonterminal('MyRule')

prod = my_rule.productions[0]
assert prod.assoc == ASSOC_LEFT
assert prod.prior == 1
assert prod.dynamic
assert prod.label == 'My Label'

with pytest.raises(AttributeError):
prod.non_existing


def test_production_meta_data_must_be_key_value():

grammar_str = r'''
MyRule: 'a' {left, 1, dynamic, nops, label:'My Label', not_allowed};
'''

with pytest.raises(ParseError, match=r'ot_allowed\*}'):
Grammar.from_string(grammar_str)


def test_terminal_meta_data():

grammar_str = r'''
MyRule: a;
terminals
a: 'a' {dynamic, 1, label: 'My Label'};
'''

grammar = Grammar.from_string(grammar_str)
term_a = grammar.get_terminal('a')

assert term_a.prior == 1
assert term_a.dynamic
assert term_a.label == 'My Label'

with pytest.raises(AttributeError):
term_a.non_existing


def test_terminal_meta_data_must_be_key_value():

grammar_str = r'''
MyRule: a;
terminals
a: 'a' {dynamic, 1, label: 'My Label', not_allowed};
'''

with pytest.raises(ParseError, match=r'ot_allowed\*}'):
Grammar.from_string(grammar_str)

0 comments on commit 3aca5dd

Please sign in to comment.