Source code for pysd.translators.vensim.vensim_section

"""
The Section class allows parsing a model section into Elements using the
SectionElementsVisitor. The final result can be exported to an
AbstractSection class in order to build a model in another programming
language.
A section is either the main model (without the macros), or a
macro definition.
"""
from typing import List, Union
from pathlib import Path
import parsimonious

from ..structures.abstract_model import AbstractElement, AbstractSection

from . import vensim_utils as vu
from .vensim_element import Element, SubscriptRange, Component


[docs]class Section(): """ The Section class allows parsing the elements of a model section. Parameters ---------- name: str Section name. That is, '__main__' for the main section, and the macro name for macros. path: pathlib.Path Section path. The model name for the main section and the clean macro name for a macro. section_type: str ('main' or 'macro') The section type. params: list List of parameters that the section takes. If it is the main section, it will be an empty list. returns: list List of variables that returns the section. If it is the main section, it will be an empty list. content: str Section content as string. split: bool If True, the created section will split the variables depending on the views_dict. views_dict: dict The dictionary of the views. Giving the variables classified at any level in order to split them by files. """ def __init__(self, name: str, path: Path, section_type: str, params: List[str], returns: List[str], content: str, split: bool, views_dict: Union[dict, None]): self.name = name self.path = path self.type = section_type self.params = params self.returns = returns self.content = content self.split = split self.views_dict = views_dict self.elements = None def __str__(self): # pragma: no cover return "\nSection: %s\n" % self.name @property def _verbose(self) -> str: # pragma: no cover """Get section information.""" text = self.__str__() if self.elements: for element in self.elements: text += element._verbose else: text += self.content return text @property def verbose(self): # pragma: no cover """Print section information to standard output.""" print(self._verbose)
[docs] def parse(self, parse_all: bool = True) -> None: """ Parse section object with parsimonious using the grammar given in 'parsing_grammars/section_elements.peg' and the class SectionElementsVisitor to visit the parsed expressions. This will break the section (__main__ or macro) in VensimElements, which are each model expression LHS and RHS with already parsed units and description. Parameters ---------- parse_all: bool (optional) If True, the created VensimElement objects will be automatically parsed. Otherwise, these objects will only be added to self.elements but not parsed. Default is True. """ # parse the section to get the elements tree = vu.Grammar.get("section_elements").parse(self.content) self.elements = SectionElementsVisitor(tree).entries if parse_all: # parse all elements self.elements = [element.parse() for element in self.elements] # split subscript from other components self.subscripts = [ element for element in self.elements if isinstance(element, SubscriptRange) ] self.components = [ element for element in self.elements if isinstance(element, Component) ] # reorder element list for better printing self.elements = self.subscripts + self.components [component.parse() for component in self.components]
[docs] def get_abstract_section(self) -> AbstractSection: """ Instantiate an object of the AbstractSection class that will be used during the building process. This method should be called after parsing the section (self.parse). This method is automatically called by Model's get_abstract_model method, and automatically generates the AbstractSubscript ranges and merges the components in elements. It also calls the get_abstract_components method from each model component. Returns ------- AbstractSection: AbstractSection AbstractSection object that can be used for building the model in another programming language. """ return AbstractSection( name=self.name, path=self.path, type=self.type, params=self.params, returns=self.returns, subscripts=[ subs_range.get_abstract_subscript_range() for subs_range in self.subscripts ], elements=self._merge_components(), split=self.split, views_dict=self.views_dict )
def _merge_components(self) -> List[AbstractElement]: """Merge model components by their name.""" merged = {} for component in self.components: # get a safe name to merge (case and white/underscore sensitivity) name = component.name.lower().replace(" ", "_") if name not in merged: # create new element if it is the first component merged[name] = AbstractElement( name=component.name, components=[]) if component.units: # add units to element data merged[name].units = component.units if component.limits: # add limits to element data merged[name].limits = component.limits if component.documentation: # add documentation to element data merged[name].documentation = component.documentation # add AbstractComponent to the list of components merged[name].components.append(component.get_abstract_component()) return list(merged.values())
class SectionElementsVisitor(parsimonious.NodeVisitor): """ Visit section elements to get their equation, units and documentation. """ # TODO include units parsing def __init__(self, ast): self.entries = [] self.visit(ast) def visit_entry(self, n, vc): self.entries.append( Element( equation=vc[0].strip(), units=vc[2].strip(), documentation=vc[4].strip(), ) ) def generic_visit(self, n, vc): return "".join(filter(None, vc)) or n.text or ""