1#######################################################################
2# Name: scoping.__init__.py
3# Purpose: Meta-model / scope providers.
4# Author: Pierre Bayerl
5# License: MIT License
6#######################################################################
7
8import glob
9import os
10import errno
11from os.path import join, exists, abspath
12
13
14def metamodel_for_file_or_default_metamodel(filename, the_metamodel):
15    from textx import metamodel_for_file
16    from textx.exceptions import TextXRegistrationError
17    try:
18        return metamodel_for_file(filename)
19    except TextXRegistrationError:
20        return the_metamodel
21
22
23# -----------------------------------------------------------------------------
24# Scope helper classes:
25# -----------------------------------------------------------------------------
26
27class Postponed(object):
28    """
29    Return an object of this class to postpone a reference resolution.
30    If you get circular dependencies in resolution logic, an error
31    is raised.
32    """
33
34
35class ModelRepository(object):
36    """
37    This class has the responsibility to hold a set of (model-identifiers,
38    model) pairs as dictionary.
39    In case of some scoping providers the model-identifier is the absolute
40    filename of the model.
41    """
42
43    def __init__(self):
44        self.name_idx = 1
45        self.filename_to_model = {}
46
47    def has_model(self, filename):
48        return abspath(filename) in self.filename_to_model
49
50    def add_model(self, model):
51        if model._tx_filename:
52            filename = abspath(model._tx_filename)
53        else:
54            filename = 'builtin_model_{}'.format(self.name_idx)
55            self.name_idx += 1
56        self.filename_to_model[filename] = model
57
58    def remove_model(self, model):
59        filename = None
60        for f, m in self.filename_to_model.items():
61            if m == model:
62                filename = f
63        if filename:
64            # print("*** delete {}".format(filename))
65            del self.filename_to_model[filename]
66
67    def __contains__(self, filename):
68        return self.has_model(filename)
69
70    def __iter__(self):
71        return iter(self.filename_to_model.values())
72
73    def __len__(self):
74        return len(self.filename_to_model)
75
76    def __getitem__(self, filename):
77        return self.filename_to_model[filename]
78
79    def __setitem__(self, filename, model):
80        self.filename_to_model[filename] = model
81
82
83class GlobalModelRepository(object):
84    """
85    This class has the responsibility to hold two ModelRepository objects:
86
87        - one for model-local visible models
88        - one for all models (globally, starting from some root model).
89
90    The second `ModelRepository` `all_models` is to cache already loaded models
91    and to prevent to load one model twice.
92
93    The class allows loading local models visible to the current model. The
94    current model is the model which references this `GlobalModelRepository` as
95    attribute `_tx_model_repository`
96
97    When loading a new local model, the current `GlobalModelRepository`
98    forwards the embedded `ModelRepository` `all_models` to the new
99    `GlobalModelRepository` of the next model. This is done using the
100    `pre_ref_resolution_callback` to set the necessary information before
101    resolving the references in the new loaded model.
102
103    """
104
105    def __init__(self, all_models=None):
106        """
107        Create a new repo for a model.
108
109        Args:
110            all_models: models to be added to this new repository.
111        """
112        self.local_models = ModelRepository()  # used for current model
113        if all_models is not None:
114            self.all_models = all_models  # used to reuse already loaded models
115        else:
116            self.all_models = ModelRepository()
117
118    def remove_model(self, model):
119        self.all_models.remove_model(model)
120        self.local_models.remove_model(model)
121
122    def remove_models(self, models):
123        for m in models:
124            self.remove_model(m)
125
126    def load_models_using_filepattern(
127            self, filename_pattern, model, glob_args, is_main_model=False,
128            encoding='utf-8', add_to_local_models=True, model_params=None):
129        """
130        Add a new model to all relevant objects.
131
132        Args:
133            filename_pattern: models to be loaded
134            model: model holding the loaded models in its _tx_model_repository
135                   field (may be None).
136            glob_args: arguments passed to the glob.glob function.
137
138        Returns:
139            the list of loaded models
140        """
141        from textx import get_metamodel
142        if model is not None:
143            self.update_model_in_repo_based_on_filename(model)
144            the_metamodel = get_metamodel(model)  # default metamodel
145        else:
146            the_metamodel = None
147        filenames = glob.glob(filename_pattern, **glob_args)
148        if len(filenames) == 0:
149            raise IOError(
150                errno.ENOENT, os.strerror(errno.ENOENT), filename_pattern)
151        loaded_models = []
152        for filename in filenames:
153            the_metamodel = metamodel_for_file_or_default_metamodel(
154                filename, the_metamodel)
155            loaded_models.append(
156                self.load_model(the_metamodel, filename, is_main_model,
157                                encoding=encoding,
158                                add_to_local_models=add_to_local_models,
159                                model_params=model_params))
160        return loaded_models
161
162    def load_model_using_search_path(
163            self, filename, model, search_path, is_main_model=False,
164            encoding='utf8', add_to_local_models=True, model_params=None):
165        """
166        Add a new model to all relevant objects
167
168        Args:
169            filename: models to be loaded
170            model: model holding the loaded models in its _tx_model_repository
171                   field (may be None).
172            search_path: list of search directories.
173
174        Returns:
175            the loaded model
176        """
177        from textx import get_metamodel
178        if model:
179            self.update_model_in_repo_based_on_filename(model)
180        for the_path in search_path:
181            full_filename = join(the_path, filename)
182            # print(full_filename)
183            if exists(full_filename):
184                if model is not None:
185                    the_metamodel = get_metamodel(model)
186                else:
187                    the_metamodel = None
188                the_metamodel = metamodel_for_file_or_default_metamodel(
189                    filename, the_metamodel)
190                return self.load_model(the_metamodel,
191                                       full_filename,
192                                       is_main_model,
193                                       encoding=encoding,
194                                       add_to_local_models=add_to_local_models,
195                                       model_params=model_params)
196
197        raise IOError(
198            errno.ENOENT, os.strerror(errno.ENOENT), filename)
199
200    def load_model(
201            self, the_metamodel, filename, is_main_model, encoding='utf-8',
202            add_to_local_models=True, model_params=None):
203        """
204        Load a single model
205
206        Args:
207            the_metamodel: the metamodel used to load the model
208            filename: the model to be loaded (if not cached)
209
210        Returns:
211            the loaded/cached model
212        """
213        assert model_params is not None,\
214            "model_params needs to be specified"
215
216        filename = abspath(filename)
217        if not self.local_models.has_model(filename):
218            if self.all_models.has_model(filename):
219                # print("CACHED {}".format(filename))
220                new_model = self.all_models[filename]
221            else:
222                # print("LOADING {}".format(filename))
223                # all models loaded here get their references resolved from the
224                # root model
225                new_model = the_metamodel.internal_model_from_file(
226                    filename, pre_ref_resolution_callback=lambda
227                    other_model: self.pre_ref_resolution_callback(other_model),
228                    is_main_model=is_main_model, encoding=encoding,
229                    model_params=model_params)
230                self.all_models[filename] = new_model
231            # print("ADDING {}".format(filename))
232            if add_to_local_models:
233                self.local_models[filename] = new_model
234        else:
235            # print("LOCALLY CACHED {}".format(filename))
236            pass
237
238        assert filename in self.all_models  # to be sure...
239        return self.all_models[filename]
240
241    def _add_model(self, model):
242        filename = self.update_model_in_repo_based_on_filename(model)
243        # print("ADDED {}".format(filename))
244        self.local_models[filename] = model
245
246    def update_model_in_repo_based_on_filename(self, model):
247        """
248        Adds a model to the repo (not initially visible)
249
250        Args:
251            model: the model to be added. If the model
252            has no filename, a name is invented
253
254        Returns:
255            the filename of the model added to the repo
256        """
257        if model._tx_filename is None:
258            for fn in self.all_models.filename_to_model:
259                if self.all_models.filename_to_model[fn] == model:
260                    # print("UPDATED/CACHED {}".format(fn))
261                    return fn
262            i = 0
263            while self.all_models.has_model("anonymous{}".format(i)):
264                i += 1
265            myfilename = "anonymous{}".format(i)
266            self.all_models[myfilename] = model
267        else:
268            myfilename = abspath(model._tx_filename)
269            if (not self.all_models.has_model(myfilename)):
270                self.all_models[myfilename] = model
271        # print("UPDATED/ADDED/CACHED {}".format(myfilename))
272        return myfilename
273
274    def pre_ref_resolution_callback(self, other_model):
275        """
276        internal: used to store a model after parsing into the repository
277
278        Args:
279            other_model: the parsed model
280
281        Returns:
282            nothing
283        """
284        filename = other_model._tx_filename
285        # print("PRE-CALLBACK -> {}".format(filename))
286        assert (filename)
287        filename = abspath(filename)
288        other_model._tx_model_repository = \
289            GlobalModelRepository(self.all_models)
290        self.all_models[filename] = other_model
291
292
293class ModelLoader(object):
294    """
295    This class is an interface to mark a scope provider as an additional model
296    loader.
297    """
298
299    def __init__(self):
300        pass
301
302    def load_models(self, model):
303        pass
304
305
306def get_all_models_including_attached_models(model):
307    """
308    get a list of all models stored within a model
309    (including the owning model).
310
311    @deprecated (BIC): use model_object.get_included_models()
312
313    Args:
314        model: the owning model
315
316    Returns:
317        a list of all models
318    """
319    return get_included_models(model)
320
321
322def get_included_models(model):
323    """
324    get a list of all models stored within a model
325    (including the owning model).
326
327    Args:
328        model: the owning model
329
330    Returns:
331        a list of all models
332    """
333    if (hasattr(model, "_tx_model_repository")):
334        models = list(model._tx_model_repository.all_models)
335        if model not in models:
336            models.append(model)
337    else:
338        models = [model]
339    return models
340
341
342def is_file_included(filename, model):
343    """
344    Determines if a file is included by a model. Also checks
345    for indirect inclusions (files included by included files).
346
347    Args:
348        filename: the file to be checked (filename is normalized)
349        model: the owning model
350
351    Returns:
352        True if the file is included, else False
353        (Note: if no _tx_model_repository is present,
354        the function always returns False)
355    """
356    if (hasattr(model, "_tx_model_repository")):
357        all_entries = model._tx_model_repository.all_models
358        return all_entries.has_model(filename)
359    else:
360        return False
361
362
363def remove_models_from_repositories(models,
364                                    models_to_be_removed):
365    """
366    Remove models from all relevant repositories (_tx_model_repository
367    of models and related metamodel(s), if applicable).
368
369    Args:
370        models: the list of models from
371               which the models_to_be_removed have to be removed.
372        models_to_be_removed: models to be removed
373
374    Returns:
375        None
376    """
377    assert isinstance(models, list)
378    for model in models:
379        if hasattr(model._tx_metamodel, "_tx_model_repository"):
380            model._tx_metamodel. \
381                _tx_model_repository.remove_models(models_to_be_removed)
382        if hasattr(model, "_tx_model_repository"):
383            model._tx_model_repository.remove_models(models_to_be_removed)
384