Skip to content

Commit

Permalink
Merge f1a5f1a into b110668
Browse files Browse the repository at this point in the history
  • Loading branch information
goto40 committed Aug 17, 2020
2 parents b110668 + f1a5f1a commit 5e77934
Show file tree
Hide file tree
Showing 9 changed files with 225 additions and 8 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ please take a look at related PRs and issues and see if the change affects you.
([#267])
- Fixed bug in `flow_dsl` test project causing static files not being included
in package build/installation. Thanks sebix@GitHub ([#272]).
- Fixed bug, where user classes not used in the grammar caused exceptions
([#270]): now, when passing a list of user classes, you need to use them in
your grammar. You can alternatively also pass a callable (see metamodel.md;
[#273]).

### Changed

Expand Down Expand Up @@ -472,6 +476,8 @@ please take a look at related PRs and issues and see if the change affects you.
- Export to dot.


[#273]: https://github.com/textX/textX/pull/273
[#270]: https://github.com/textX/textX/issues/270
[#272]: https://github.com/textX/textX/pull/272
[#267]: https://github.com/textX/textX/issues/267
[#266]: https://github.com/textX/textX/issues/266
Expand Down
7 changes: 7 additions & 0 deletions docs/metamodel.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@ represent.
Now `entity_mm` can be used to parse the input models where our `Entity` class
will be instantiated to represent each `Entity` rule from the grammar.

When passing a list of classes (as shown in the example above), you need to have rules
for all of these classes in your grammar (else, you get an exception). Alternatively,
you can also pass a callable (instead of a list of classes) to return user classes
given a rule name. In that case, only rule names found in the grammar
are used to query user classes.
See [unittest](https://github.com/textX/textX/blob/master/tests/functional/regressions/test_issue270.py).

!!! note
Constructor of the user-defined classes should accept all attributes defined
by the corresponding rule from the grammar. In the previous example, we have
Expand Down
105 changes: 105 additions & 0 deletions tests/functional/regressions/test_issue270.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
from __future__ import unicode_literals
from textx.metamodel import metamodel_from_str
from pytest import raises
from textx.exceptions import TextXSemanticError

grammar = """
MyModel: 'model' name=ID
connections+=Connection
sender+=Sender
receiver+=Receiver;
Sender:
'outgoing' name=ID 'over' connection=[Connection|ID];
Receiver:
'incoming' name=ID 'over' connection=[Connection|ID];
Connection:
'connection' name=ID
'port' port=STRING
'ip' ip=STRING;
"""


grammar_with_baseclass_fix = """
MyModel: 'model' name=ID
connections+=Connection
sender+=Sender
receiver+=Receiver;
Sender:
'outgoing' name=ID 'over' connection=[Connection|ID];
Receiver:
'incoming' name=ID 'over' connection=[Connection|ID];
// fix/works (no unused user classes):
ConnectionHandler: Sender|Receiver;
Connection:
'connection' name=ID
'port' port=STRING
'ip' ip=STRING;
"""


modelstring = """
model Example
connection conn port "1" ip "127.0.0.1"
outgoing out0 over conn
incoming in0 over conn
"""


class ConnectionHandler(object):
def _init_(self):
print('')

def awesomeMethod4SenderAndReceiver(self):
print("I am really important for Sender and Receiver")


class Sender(ConnectionHandler):
def __init__(self, name=None, connection=None, parent=None):
super(Sender, self).__init__()
print('')


class Receiver(ConnectionHandler):
def __init__(self, name=None, connection=None, parent=None):
super(Receiver, self).__init__()
print('')


def test_issue270():
# fix/works (no unused user classes):
mm = metamodel_from_str(grammar, classes=[Sender, Receiver])
_ = mm.model_from_str(modelstring)

# fix/works (no unused user classes; see grammar_with_baseclass_fix):
mm = metamodel_from_str(grammar_with_baseclass_fix,
classes=[ConnectionHandler, Sender, Receiver])
_ = mm.model_from_str(modelstring)

# does not work
with raises(TextXSemanticError,
match="ConnectionHandler class is not used in the grammar"):
_ = metamodel_from_str(grammar, classes=[ConnectionHandler, Sender, Receiver])

# does work (allow unused user classes by providing a callable instead of
# a list of classes: the callable returns a user class for a given rule name
# or None)
def class_provider(name):
classes = [ConnectionHandler, Sender, Receiver]
classes = dict(map(lambda x: (x.__name__, x), classes))
return classes.get(name)

mm = metamodel_from_str(grammar, classes=class_provider)
m = mm.model_from_str(modelstring)
for s in m.sender:
assert isinstance(s, Sender)
for r in m.receiver:
assert isinstance(r, Receiver)
49 changes: 48 additions & 1 deletion tests/functional/test_user_classes.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
from __future__ import unicode_literals
import pytest # noqa
from textx import metamodel_from_str
from textx import metamodel_from_str, metamodel_from_file
from os.path import join, dirname, abspath
from pytest import raises
from textx.exceptions import TextXSemanticError


grammar = """
First:
Expand Down Expand Up @@ -67,3 +71,46 @@ def __init__(self, parent, sec):

# Check additional attributes
assert model.some_attr == 1


class Thing(object):
def __init__(self, **kwargs):
for k in kwargs.keys():
setattr(self, k, kwargs[k])


class AThing(object):
def __init__(self, **kwargs):
for k in kwargs.keys():
setattr(self, k, kwargs[k])


class BThing(object):
def __init__(self, **kwargs):
for k in kwargs.keys():
setattr(self, k, kwargs[k])


def test_user_class_with_imported_grammar():
this_folder = dirname(abspath(__file__))
mm = metamodel_from_file(join(this_folder, "user_classes", "B.tx"),
classes=[AThing, BThing])
m = mm.model_from_str("""
A 2,1
B Hello
""")
assert m.a.v.x == 2
assert m.a.v.y == 1
assert m.b.v.name == "Hello"
assert type(m.b.v).__name__ == "Thing"
assert type(m.a.v).__name__ == "Thing"
assert isinstance(m.a, AThing)
assert isinstance(m.b, BThing)

with raises(TextXSemanticError,
match=r'.*redefined imported rule Thing'
+ r' cannot be replaced by a user class.*'):
mm = metamodel_from_file(join(this_folder, "user_classes", "B.tx"),
classes=[AThing, BThing, Thing])
# now, all involved user classes **may** be instrumented...
# (after an exception, we do not guarantee 100% cleanup of user classes)
2 changes: 2 additions & 0 deletions tests/functional/user_classes/A.tx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Thing: x=INT "," y=INT;
AThing: "A" v=Thing;
9 changes: 9 additions & 0 deletions tests/functional/user_classes/B.tx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import A

Model:
a=AThing
b=BThing
;

Thing: name=ID;
BThing: "B" v=Thing;
18 changes: 15 additions & 3 deletions textx/lang.py
Original file line number Diff line number Diff line change
Expand Up @@ -535,9 +535,21 @@ def visit_rule_name(self, node, children):
self.dprint("Creating class: {}".format(rule_name))

# If a class is given by the user use it. Else, create new class.
if rule_name in self.metamodel.user_classes:

cls = self.metamodel.user_classes[rule_name]
if self.metamodel.user_classes_provider is not None:
cls = self.metamodel.user_classes_provider(
rule_name
)
if cls is not None:
self.metamodel.user_classes[rule_name] = cls
else:
cls = self.metamodel.user_classes.get(rule_name)

if cls is not None:
if rule_name in self.metamodel._used_rule_names_for_user_classes:
raise TextXSemanticError("redefined imported rule"
+ " {}".format(rule_name)
+ " cannot be replaced by a user class")
self.metamodel._used_rule_names_for_user_classes.add(rule_name)

# Initialize special attributes
self.metamodel._init_class(cls, None, node.position,
Expand Down
35 changes: 31 additions & 4 deletions textx/metamodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,16 @@ def metamodel_from_str(lang_desc, metamodel=None, **kwargs):
"""

is_main_metamodel = metamodel is None

if not metamodel:
metamodel = TextXMetaModel(**kwargs)

language_from_str(lang_desc, metamodel)

if is_main_metamodel:
metamodel.validate_user_classes()

return metamodel


Expand Down Expand Up @@ -121,8 +126,10 @@ class TextXMetaModel(DebugPrinter):
builtins(dict): A dict of named object used in linking phase.
References to named objects not defined in the model will be
searched here.
classes(list of classes): A list of user supplied classes to use
instead of the dynamically created.
classes(list of classes or callable): A list of user supplied classes
to use instead of the dynamically created or a callable providing
those classes. The callable must accept a rule name and return a
class for that rule name or None.
obj_processors(dict): A dict of user supplied object processors keyed
by rule/class name (may be a fully qualified name).
rootcls(TextXClass): A language class that is a root of the meta-model.
Expand Down Expand Up @@ -176,9 +183,14 @@ def __init__(self, file_name=None, classes=None, builtins=None,

# Convert classes to dict for easier lookup
self.user_classes = {}
self.user_classes_provider = None
self._used_rule_names_for_user_classes = set()
if classes:
for c in classes:
self.user_classes[c.__name__] = c
if callable(classes):
self.user_classes_provider = classes
else:
for c in classes:
self.user_classes[c.__name__] = c

self.auto_init_attributes = auto_init_attributes
self.ignore_case = ignore_case
Expand Down Expand Up @@ -486,6 +498,21 @@ def validate(self):
textX rules.
"""
# TODO: Implement complex textX validations.
pass

def validate_user_classes(self):
"""
Validates user classes of the meta model.
Called after construction of the main metamodel (not
imported ones).
"""
from textx.exceptions import TextXSemanticError
for user_class in self.user_classes.values():
if user_class.__name__ not in self._used_rule_names_for_user_classes:
# It is not a user class used in the grammar
raise TextXSemanticError(
"{} class is not used in the grammar".format(
user_class.__name__))

def __getitem__(self, name):
"""
Expand Down
2 changes: 2 additions & 0 deletions textx/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,8 @@ def get_model_from_str(self, model_str, file_name=None, debug=None,
return model

def _replace_user_attr_methods_for_class(self, user_class):
assert hasattr(user_class, "_tx_obj_attrs")

# Custom attr dunder methods used for user classes during loading
def _getattribute(obj, name):
if name == '__dict__':
Expand Down

0 comments on commit 5e77934

Please sign in to comment.