Skip to content

Commit

Permalink
Merge d8fe12b into c4a781c
Browse files Browse the repository at this point in the history
  • Loading branch information
goto40 committed May 3, 2020
2 parents c4a781c + d8fe12b commit 2078df1
Show file tree
Hide file tree
Showing 10 changed files with 331 additions and 21 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ please take a look at related PRs and issues and see if the change affects you.
- `textx version` command ([#219])
- Versions for languages/packages in `list-languages` and `list-generators`
commands ([#228])
- Added the ability to specify extra parameters during `model_from_file` or
`model_from_str` and to define which extra parameters exist in the
metamodel ([#243]).

### Fixed

Expand Down Expand Up @@ -444,7 +447,7 @@ please take a look at related PRs and issues and see if the change affects you.
- Metamodel and model construction.
- Export to dot.


[#243]: https://github.com/textX/textX/pull/243
[#235]: https://github.com/textX/textX/pull/235
[#234]: https://github.com/textX/textX/pull/234
[#233]: https://github.com/textX/textX/pull/233
Expand Down
11 changes: 11 additions & 0 deletions docs/model.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,14 @@ The model may have a model repository (initiated by some scope provider or by
the metamodel). This object is responsible to provide and cache other model
instances (see textx.scoping.providers).

### _tx_model_params

This attribute always exists. It holds all additional parameters passed to
`model_from_str` or `model_from_file` of a metamodel. These parameters are
restricted by the `metamodel._tx_model_param_definitions` object, which is
controlled by the metamodel designer.

`metamodel._tx_model_param_definitions` can be queried (like a dict) to
retrieve possible extra parameters and their description for a metamodel.
It is also used to restrict the additional parameters passed to
`model_from_str` or `model_from_file`.
96 changes: 96 additions & 0 deletions tests/functional/test_metamodel/test_model_params.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
from __future__ import unicode_literals
from textx import (metamodel_from_str)
import os.path
from pytest import raises
from textx.exceptions import TextXError


grammar = r"""
Model: 'MyModel' name=ID;
"""

text = r"""
MyModel test1
"""


def test_model_params():
mm = metamodel_from_str(grammar)
mm._tx_model_param_definitions.add(
"parameter1", "an example param (1)"
)
mm._tx_model_param_definitions.add(
"parameter2", "an example param (2)"
)

m = mm.model_from_str(text, parameter1='P1', parameter2='P2')
assert m.name == 'test1'
assert hasattr(m, '_tx_model_params')
assert len(m._tx_model_params) == 2
assert len(m._tx_model_params.used_keys) == 0

assert not m._tx_model_params.all_used

assert m._tx_model_params['parameter1'] == 'P1'
assert len(m._tx_model_params.used_keys) == 1
assert 'parameter1' in m._tx_model_params.used_keys
assert 'parameter2' not in m._tx_model_params.used_keys

assert not m._tx_model_params.all_used

assert m._tx_model_params['parameter2'] == 'P2'
assert len(m._tx_model_params.used_keys) == 2
assert 'parameter1' in m._tx_model_params.used_keys
assert 'parameter2' in m._tx_model_params.used_keys

assert m._tx_model_params.all_used

assert m._tx_model_params.get(
'missing_params', default='default value') == 'default value'
assert m._tx_model_params.get(
'parameter1', default='default value') == 'P1'

with raises(TextXError, match=".*unknown parameter myerror2.*"):
mm.model_from_str(text, parameter1='P1', myerror2='P2')

assert len(mm._tx_model_param_definitions) >= 2
assert 'parameter1' in mm._tx_model_param_definitions
assert 'parameter1' in mm._tx_model_param_definitions
assert mm._tx_model_param_definitions[
'parameter1'].description == "an example param (1)"


def test_model_params_empty():
mm = metamodel_from_str(grammar)
mm._tx_model_param_definitions.add(
"parameter1", "an example param (1)"
)
mm._tx_model_param_definitions.add(
"parameter2", "an example param (2)"
)

m = mm.model_from_str(text)
assert m.name == 'test1'
assert hasattr(m, '_tx_model_params')
assert len(m._tx_model_params) == 0

assert m._tx_model_params.all_used


def test_model_params_file_based():
mm = metamodel_from_str(grammar)
mm._tx_model_param_definitions.add(
"parameter1", "an example param (1)"
)
mm._tx_model_param_definitions.add(
"parameter2", "an example param (2)"
)

current_dir = os.path.dirname(__file__)
m = mm.model_from_file(
os.path.join(current_dir, 'test_model_params',
'model.txt'),
parameter1='P1', parameter2='P2')
assert m.name == 'file_based'
assert hasattr(m, '_tx_model_params')
assert len(m._tx_model_params) == 2
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
MyModel file_based
38 changes: 38 additions & 0 deletions tests/functional/test_scoping/test_global_import_modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from textx.scoping.tools import get_unique_named_object, \
check_unique_named_object_has_class

from pytest import raises


def test_globalimports_basic_test_with_single_model_file():
"""
Expand Down Expand Up @@ -151,3 +153,39 @@ def test_globalimports_basic_test_with_distributed_model():
#################################
# END
#################################


def test_globalimports_with_project_root_model_parameter():
"""
Basic test for the FQNGlobalRepo.
Tests that "project_root" model parameter has an effect.
"""
#################################
# META MODEL DEF
#################################

my_meta_model = metamodel_from_file(
join(abspath(dirname(__file__)), 'interface_model2',
'Interface.tx'))
my_meta_model.register_scope_providers(
{"*.*": scoping_providers.FQNGlobalRepo(
join("model_a", "*.if"))})

#################################
# MODEL PARSING
#################################

with raises(IOError):
_ = my_meta_model.model_from_file(
join(abspath(dirname(__file__)), "interface_model2",
"model_a", "all_in_one.if"))

_ = my_meta_model.model_from_file(
join(abspath(dirname(__file__)), "interface_model2",
"model_a", "all_in_one.if"),
project_root=join(
abspath(dirname(__file__)), "interface_model2"))

#################################
# END
#################################
51 changes: 43 additions & 8 deletions textx/metamodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
RULE_MATCH, RULE_ABSTRACT
from textx.exceptions import TextXError
from .registration import LanguageDesc, metamodel_for_language
from .model_params import ModelParams, ModelParamDefinitions

if sys.version < '3':
text = unicode # noqa
Expand Down Expand Up @@ -164,6 +165,10 @@ def __init__(self, file_name=None, classes=None, builtins=None,

super(TextXMetaModel, self).__init__(**kwargs)

self._tx_model_param_definitions = ModelParamDefinitions()
self._tx_model_param_definitions.add(
"project_root", "the project root")

self.file_name = file_name
self.rootcls = None

Expand Down Expand Up @@ -547,43 +552,67 @@ def _current_namespace(self):
return self.namespaces[self._namespace_stack[-1]]

def model_from_str(self, model_str, file_name=None, debug=None,
pre_ref_resolution_callback=None, encoding='utf-8'):
pre_ref_resolution_callback=None, encoding='utf-8',
**kwargs):
"""
Instantiates model from the given string.
:param pre_ref_resolution_callback: called before references are
resolved. This can be useful to manage models distributed
across files (scoping)
:param **kwargs additional arguments available through
_tx_model_params after initiating the model. The attribute
is set while executing pre_ref_resolution_callback (see
scoping.md)
"""
self._tx_model_param_definitions.check_params_and_raise_on_error(
'from_str', **kwargs)

if type(model_str) is not text:
raise TextXError("textX accepts only unicode strings.")

if file_name is None:
def kwargs_callback(other_model):
if hasattr(other_model, '_tx_metamodel'):
other_model._tx_model_params = ModelParams(kwargs)
if pre_ref_resolution_callback:
pre_ref_resolution_callback(other_model)

model = self._parser_blueprint.clone().get_model_from_str(
model_str, debug=debug,
pre_ref_resolution_callback=pre_ref_resolution_callback)
pre_ref_resolution_callback=kwargs_callback)

for p in self._model_processors:
p(model, self)
else:
model = self.internal_model_from_file(file_name, encoding, debug,
model_str=model_str)
model = self.internal_model_from_file(
file_name, encoding, debug,
model_str=model_str,
pre_ref_resolution_callback=pre_ref_resolution_callback,
model_params=ModelParams(kwargs))

return model

def model_from_file(self, file_name, encoding='utf-8', debug=None):
return self.internal_model_from_file(file_name, encoding, debug)
def model_from_file(self, file_name, encoding='utf-8', debug=None,
**kwargs):
self._tx_model_param_definitions.check_params_and_raise_on_error(
file_name, **kwargs)

return self.internal_model_from_file(
file_name, encoding, debug,
model_params=ModelParams(kwargs))

def internal_model_from_file(
self, file_name, encoding='utf-8', debug=None,
pre_ref_resolution_callback=None, is_main_model=True,
model_str=None):
model_str=None, model_params=None):
"""
Instantiates model from the given file.
:param pre_ref_resolution_callback: called before references are
resolved. This can be useful to manage models distributed
across files (scoping)
"""
assert model_params is not None,\
"model_params are required in all cases"
file_name = abspath(file_name)
model = None
callback = pre_ref_resolution_callback
Expand All @@ -606,6 +635,12 @@ def _pre_ref_resolution_callback(other_model):
model = self._tx_model_repository.all_models\
.filename_to_model[file_name]

def kwargs_callback(other_model):
if hasattr(other_model, '_tx_metamodel'):
other_model._tx_model_params = model_params
if callback:
callback(other_model)

if not model:
# Read model from file
if not model_str:
Expand All @@ -614,7 +649,7 @@ def _pre_ref_resolution_callback(other_model):
# model not present (from global repo) -> load it
model = self._parser_blueprint.clone().get_model_from_str(
model_str, file_name, debug=debug, encoding=encoding,
pre_ref_resolution_callback=callback,
pre_ref_resolution_callback=kwargs_callback,
is_main_model=is_main_model)

for p in self._model_processors:
Expand Down
3 changes: 3 additions & 0 deletions textx/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,9 @@ def call_obj_processors(metamodel, model_obj,
if pre_ref_resolution_callback:
pre_ref_resolution_callback(model)

if hasattr(model, '_tx_metamodel'):
assert hasattr(model, '_tx_model_params')

for scope_provider in metamodel.scope_providers.values():
from textx.scoping import ModelLoader
if isinstance(scope_provider, ModelLoader):
Expand Down

0 comments on commit 2078df1

Please sign in to comment.