diff --git a/python/export_trace_manager.py b/python/export_trace_manager.py index adc4902a4239fa46f69cc3401fa6012ca7bfba3b..1c2025b1d3a9ee4012da57f473ab995c3defa7fa 100644 --- a/python/export_trace_manager.py +++ b/python/export_trace_manager.py @@ -8,7 +8,7 @@ from message_popup import message_popup from preferences import preferences from settings import settings from mp_file_dialog import get_save_filename, get_existing_directory -from mp_code_event_dict import mp_code_schema +from mp_code_parser import mp_code_schema from export_trace import export_trace def _image_format(): diff --git a/python/filter_event_item_delegates.py b/python/filter_event_item_delegates.py index d9f48af6c228bc9efa21f5a63d343c5ad61aa989..eb88968e25bcfa1c3d1bef7e1c0c9a5f4628dcc2 100644 --- a/python/filter_event_item_delegates.py +++ b/python/filter_event_item_delegates.py @@ -1,6 +1,6 @@ from PySide6.QtCore import Qt, Signal, Slot from PySide6.QtWidgets import QStyledItemDelegate, QComboBox, QSpinBox -from mp_code_event_dict import mp_code_event_dict, mp_code_event_list +from mp_code_parser import mp_code_event_dict, mp_code_event_list """ Provides widgets for the cells for relationship and recurrence event filters. diff --git a/python/graph_list_table_model.py b/python/graph_list_table_model.py index 6d51cc9cee6607eb1a0163c12cd8ccf87820460e..1dfc34cabd5e42dd8c8122d02f7511f8fc8a810a 100644 --- a/python/graph_list_table_model.py +++ b/python/graph_list_table_model.py @@ -1,5 +1,5 @@ from PySide6.QtCore import Qt, QAbstractTableModel, QModelIndex -from say_info import say_column_names, say_column_map, say_info +from mp_code_parser import mp_code_say_headers, say_info # graph list model column constants GRAPH_COLUMN = 0 # the graph item @@ -35,13 +35,18 @@ class GraphListTableModel(QAbstractTableModel): self.graphs = list() self.headers = list() - self.say_map = say_column_map(self.graphs, len(_HARDCODED_HEADERS)) - def set_graph_list(self, graphs): - self.beginResetModel() # because self.graphs change + def set_graph_list(self, graphs, mp_code): + self.beginResetModel() + + # set the data self.graphs = graphs + + # set the headers + say_headers = mp_code_say_headers(mp_code) self.headers = _HARDCODED_HEADERS.copy() - self.headers.extend(say_column_names(graphs)) + self.headers.extend(say_headers) + self.endResetModel() def headerData(self, section, orientation, @@ -84,8 +89,12 @@ class GraphListTableModel(QAbstractTableModel): gry_trace = gry_graph["trace"] return len(gry_trace["nodes"]) + len(gry_trace["edges"]) if column >= len(self.headers): + # column number out of range raise RuntimeError("bad %d"%column) + # get the column value based on SAY text in the gry_graph + return say_info(gry_graph, self.headers[column]) + # provide values for columns dynamically created from SAY nodes header = self.headers[column] says = say_info(gry_graph) diff --git a/python/graph_metadata_label.py b/python/graph_metadata_label.py index 1e9484b249687dafcfa725b41b6adfc33816d4fd..439e573b32b34e0bdf05ba077bbed2d7953d06de 100644 --- a/python/graph_metadata_label.py +++ b/python/graph_metadata_label.py @@ -1,7 +1,7 @@ from PySide6.QtCore import QObject # for signal/slot support from PySide6.QtCore import Slot from PySide6.QtWidgets import QLabel -from mp_code_event_dict import mp_code_schema +from mp_code_parser import mp_code_schema class GraphMetadataLabel(QObject): """Provides the active scope and MP Code schema name in a QLabel.""" diff --git a/python/graphs_manager.py b/python/graphs_manager.py index 5e9d84c383c7f6d79875558d89b36526a86b0252..1370050b714b9f52fecfa5b2918f0affc6003b74 100644 --- a/python/graphs_manager.py +++ b/python/graphs_manager.py @@ -52,7 +52,7 @@ class GraphsManager(QObject): # clear graphs self.graphs = list() self.scope = scope - self.graph_list_table_model.set_graph_list(list()) + self.graph_list_table_model.set_graph_list(list(), "") # repopulate graphs log_to_statusbar("Creating %d graphs..."%len(gry_graphs)) @@ -61,7 +61,7 @@ class GraphsManager(QObject): log_to_statusbar("Done creating %d graphs"%len(gry_graphs)) # set graphs - self.graph_list_table_model.set_graph_list(self.graphs) + self.graph_list_table_model.set_graph_list(self.graphs, self.mp_code) # remember spacing for reference in case spacing changes self._remember_spacing() @@ -73,7 +73,7 @@ class GraphsManager(QObject): self.mp_code = "" self.graphs = list() self.scope = 1 - self.graph_list_table_model.set_graph_list(list()) + self.graph_list_table_model.set_graph_list(list(), "") # signal self.signal_graphs_loaded.emit() @@ -84,7 +84,7 @@ class GraphsManager(QObject): self.mp_code = existing_mp_code self.graphs = existing_graphs self.scope = existing_scope - self.graph_list_table_model.set_graph_list(self.graphs) + self.graph_list_table_model.set_graph_list(self.graphs, self.mp_code) # signal self.signal_graphs_loaded.emit() diff --git a/python/gry_manager.py b/python/gry_manager.py index e23e72eed61b6d793dc112b376ab512da1eaeeb5..1ed967d05ee121544044aadc4ec355efba914423 100644 --- a/python/gry_manager.py +++ b/python/gry_manager.py @@ -7,7 +7,7 @@ from settings import settings import mp_json_io_manager from mp_file_dialog import get_open_filename, get_save_filename from mp_logger import log, begin_timestamps, timestamp, show_timestamps -from mp_code_event_dict import mp_code_schema +from mp_code_parser import mp_code_schema from version_file import VERSION from message_popup import message_popup diff --git a/python/gui_manager.py b/python/gui_manager.py index d9f4eee822433f0cce138234c7292f118b4a7c16..34948c60a44ccb3b92d96879dcef16d2e83af1b6 100644 --- a/python/gui_manager.py +++ b/python/gui_manager.py @@ -77,7 +77,7 @@ from report_an_issue import report_an_issue from startup_options import parse_startup_options from mp_file_dialog import get_open_filename, get_save_filename, \ get_existing_directory -from mp_code_event_dict import mp_code_schema +from mp_code_parser import mp_code_schema from mp_code_filename import active_mp_code_filename, \ set_active_mp_code_filename from verbose import verbose diff --git a/python/model_statistics.py b/python/model_statistics.py index cd0beae1b2ef72b74fbb79c10ef66902d0bf635a..665363df4c748cd77855069eb381b7c334a542df 100644 --- a/python/model_statistics.py +++ b/python/model_statistics.py @@ -1,7 +1,7 @@ from version_file import VERSION from collections import defaultdict from hashlib import blake2b # supposed to be fast -from mp_code_event_dict import mp_code_event_dict, mp_code_schema +from mp_code_parser import mp_code_event_dict, mp_code_schema from verbose import verbose _EVENT_TYPES = ["root", "composite", "atomic"] diff --git a/python/mp_code_manager.py b/python/mp_code_manager.py index 5510cdae9322b4cd22ec797557e020a35d97efcd..c641edfe08c2abcd8f3c23fc54a9920c3327c6e7 100644 --- a/python/mp_code_manager.py +++ b/python/mp_code_manager.py @@ -5,7 +5,7 @@ from PySide6.QtGui import QTextDocument from PySide6.QtWidgets import QPlainTextDocumentLayout from mp_code_syntax_highlighter import MPCodeSyntaxHighlighter from mp_code_syntax_checker import mp_check_syntax -from mp_code_event_dict import mp_code_schema +from mp_code_parser import mp_code_schema from mp_code_filename import set_active_mp_code_filename # use this to signal completion to GUI. diff --git a/python/mp_code_event_dict.py b/python/mp_code_parser.py similarity index 60% rename from python/mp_code_event_dict.py rename to python/mp_code_parser.py index 3202f80a2d486a088a1122b5bad875ec978f1a7e..e8ca8e36759ddb6551b9327739ebca51f3eb9544 100644 --- a/python/mp_code_event_dict.py +++ b/python/mp_code_parser.py @@ -1,4 +1,6 @@ +import re from copy import deepcopy +from collections import defaultdict from PySide6.QtCore import QRegularExpression from mp_code_expressions import MP_KEYWORDS, MP_META_SYMBOL_EXPRESSION, \ COMMENT_START_EXPRESSION, \ @@ -17,6 +19,15 @@ _STARTING_EVENT_COLON_EXPRESSION \ _EVENT_WORD_EXPRESSION \ = QRegularExpression(r"(\$\$[a-zA-Z_]\w*|[a-zA-Z_]\w*)") +# SAY keyword +_SAY_KEYWORD_EXPRESSION = QRegularExpression(r"\bSAY\s*\(") + +# SAY content +_SAY_CONTENT = QRegularExpression(r"\bSAY\s*\((.*)\)\s*$") + +# increment_report_command "=>" (97) or add_tuple_command "<|" (106) +_SAY_EXEMPTION_EXPRESSION = QRegularExpression(r"=\s*>|<\s*\|") + # MP keywords _STARTING_KEYWORD_EXPRESSION = QRegularExpression(r"^\s*(%s)"%MP_KEYWORDS) @@ -29,20 +40,16 @@ _mp_code_text_cache = "" _mp_lines_cache = "" _event_dict = deepcopy(_EMPTY_EVENT_DICT) -# get list of lines of text with comments and quoted text removed -# and reorganized to delimit lines by ; instead of \n. -def _parsable_mp_lines(mp_code_text): +# return non-commented lines +def _non_comment_lines(mp_code_text): in_comment = False - in_custom_view_declaration = False - text_start_index = 0 stripped_lines = list() - parts = list() lines = mp_code_text.split("\n") for line in lines: # remove multi-line comments text_start_index = 0 - parts.clear() + parts = list() while text_start_index != -1: if in_comment == False: # in non-comment @@ -80,26 +87,30 @@ def _parsable_mp_lines(mp_code_text): if single_line_comment_index != -1: stripped_line = stripped_line[0:single_line_comment_index] - # remove quoted text - while True: - match = QUOTED_TEXT_EXPRESSION.match(stripped_line) - quoted_text_index = match.capturedStart() - if quoted_text_index == -1: - # no quoted text - break - # remove quoted text including quotation marks - stripped_line = stripped_line[:quoted_text_index] \ - + stripped_line[match.capturedEnd():] - - # keep non-empty lines - if stripped_line != "\n": - stripped_lines.append(stripped_line) - - # convert CR-delimited lines into semicolon-delimited lines + stripped_lines.append(stripped_line) + + # convert list of stripped lines into a list made from semicolon boundaries parsable_mp_lines = (" ".join(stripped_lines)).split(";") return parsable_mp_lines +def _non_quote_line(non_comment_line): + # remove quoted text + while True: + match = QUOTED_TEXT_EXPRESSION.match(non_comment_line) + quoted_text_index = match.capturedStart() + if quoted_text_index == -1: + # no quoted text + break + # remove quoted text including quotation marks + non_comment_line = non_comment_line[:quoted_text_index] \ + + non_comment_line[match.capturedEnd():] + return non_comment_line + +def _non_quote_lines(non_comment_lines): + non_quote_lines = [_non_quote_line(line) for line in non_comment_lines] + return non_quote_lines + # return tuple schema, remainder def _parse_schema(mp_line): match = _STARTING_SCHEMA_EXPRESSION.match(mp_line) @@ -151,9 +162,9 @@ def mp_code_event_dict(mp_code_text): return deepcopy(_event_dict) _mp_code_text_cache = mp_code_text - # convert text into lines without comments or quoted text and delineated - # with ; instead of \n. - mp_lines = _parsable_mp_lines(mp_code_text) + # convert text into lines without comments or quoted text + non_comment_lines = _non_comment_lines(mp_code_text) + mp_lines = _non_quote_lines(non_comment_lines) # optimization if mp_lines == _mp_lines_cache: @@ -205,3 +216,91 @@ def mp_code_event_list(event_dict): def mp_code_schema(mp_code_text): return mp_code_event_dict(mp_code_text)["schema"] +def _say_text_and_number_count(say_line): + say_content = _SAY_CONTENT.match(say_line).captured(1) + + quoted_text = "" + between_text = list() + number_count = 0 + + global_match = QUOTED_TEXT_EXPRESSION.globalMatch(say_content) + + # text and count + start = 0 + while global_match.hasNext(): + match = global_match.next() + quoted_text += match.captured()[1:-1] # text with quotes stripped + captured_start = match.capturedStart() + between_text = say_content[start:match.capturedStart()] + if between_text.strip(): + number_count += 1 + start = match.capturedEnd() + + # any final count + final_text = say_content[start:] + if final_text.strip(): + number_count += 1 + + return quoted_text, number_count + +# derive sorted say headers from mp_code_text +# We do not cache these results. This mp_code_text is from trace generation +# and is called by graph_list_table_model when the graph is loaded. +def mp_code_say_headers(mp_code_text): + + # convert text into lines without comments + non_comment_lines = _non_comment_lines(mp_code_text) + + header_list = list() + for line in non_comment_lines: + non_quote_line = _non_quote_line(line) + + # SAY keyword is required + if not _SAY_KEYWORD_EXPRESSION.globalMatch(non_quote_line).hasNext(): + continue + + # SAY directives increment_report_command "=>" (97) and + # add_tuple_command "<|" (106) do not generate SAY event blocks + if _SAY_EXEMPTION_EXPRESSION.globalMatch(non_quote_line).hasNext(): + continue + + say_text, number_count = _say_text_and_number_count(line) + + # add to texts and columns + if number_count == 0: + header_list.append(say_text) + else: + for i in range(number_count): + header_list.append("%s (%d)"%(say_text, i+1)) + + header_list = sorted(header_list, key=str.casefold) + return header_list + +# Return text for the matching column else X or checkmark +# Say nodes are type="T".""" +_number_pattern = re.compile(r"[0-9.]+") +def say_info(gry_graph, column_title): + if not "trace" in gry_graph: + raise RuntimeError("bad") + + for gry_node in gry_graph["trace"]["nodes"]: + if gry_node["type"] == "T": + + # try exact column match + label = gry_node["label"] + if label == column_title: + return "\u2713" # checkmark + + # match the label with numbers extracted with the column title + prefix = _number_pattern.sub("", label) + numbers = _number_pattern.findall(label) + for i, number in enumerate(numbers): + # key is column without numbers and with index appended + key = "%s (%i)"%(prefix, i+1) + if key == column_title: + # column matches for this index so return its number + return float(numbers[i]) + + # no column match found + return "X" + diff --git a/python/mp_code_syntax_highlighter.py b/python/mp_code_syntax_highlighter.py index 8617cb3e190c37dc4fa2e580fe2ee7427ea70cf5..31cd006f2fd5ea6ec10b5f015a1bfacfc6ab0217 100644 --- a/python/mp_code_syntax_highlighter.py +++ b/python/mp_code_syntax_highlighter.py @@ -9,7 +9,7 @@ from mp_code_expressions import ATOMIC_NAME_EXPRESSION, \ MP_KEYWORD_EXPRESSION, MP_META_SYMBOL_EXPRESSION, \ OPERATOR_EXPRESSION, NUMBER_EXPRESSION, \ VARIABLE_EXPRESSION, QUOTED_TEXT_EXPRESSION -from mp_code_event_dict import mp_code_event_dict +from mp_code_parser import mp_code_event_dict from spellcheck import unknown_words_regex """ diff --git a/python/say_info.py b/python/say_info.py deleted file mode 100644 index ad4615a4863bc68084d3f753db119020c627d082..0000000000000000000000000000000000000000 --- a/python/say_info.py +++ /dev/null @@ -1,46 +0,0 @@ -import re -from collections import defaultdict -"""Extract SAY titles and numbers from SAY trace nodes.""" - -_pattern = re.compile(r"[0-9.]+") - -# Return {say text, [numbers]} where: -# * say_text is say text with index prepended and numbers removed -# * and numbers is a list of zero or more numbers. -# Say nodes are type="T".""" -def say_info(gry_graph): - if not "trace" in gry_graph: - raise RuntimeError("bad") - - says = defaultdict(list) - for gry_node in gry_graph["trace"]["nodes"]: - if gry_node["type"] == "T": - label = gry_node["label"] - key = _pattern.sub(" ", label) - numbers = _pattern.findall(label) - for i, number in enumerate(numbers, 1): - try: - says["%i: %s"%(i, key)] = float(number) - except Exception as e: - # multiple decimal places can thwart _pattern so manage - # invalid sortable numbers as text - print("say_info.say_info: invalid number: %s"%number) - says["%i: %s"%(i, key)] = number - return says - -# Return a list of "say text" column names. -def say_column_names(graphs): - if len(graphs) >= 2: - says = say_info(graphs[1].gry_graph) - return sorted(says.keys()) - else: - return list() - -# Return a dict of {key="say text", value=column index} -def say_column_map(graphs, start_index): - column_names = say_column_names(graphs) - column_map = dict() - for i, column_name in enumerate(column_names, start_index): - column_map[column_name] = i - return column_map - diff --git a/python/session_snapshots.py b/python/session_snapshots.py index 95f8ba92da0007309e9917fb674afcabf40f8817..b7aa7efabb32277b5f756ef69cc458635a62980a 100644 --- a/python/session_snapshots.py +++ b/python/session_snapshots.py @@ -4,7 +4,7 @@ from time import strftime from PySide6.QtGui import QAction from paths_gryphon import SNAPSHOTS_PATH from session_snapshots_dialog_wrapper import SessionSnapshotsDialogWrapper -from mp_code_event_dict import mp_code_schema +from mp_code_parser import mp_code_schema def schema_filename(schema_name): # filename = <schema name>_yy_mm_dd-hh_mm_ss.gry diff --git a/python/test.py b/python/test.py index fabcf3a44fbfc3885c60eceb91adc12587db145e..e3017cd0f89e54ea7bf9c90e322a1b1755d32c5b 100755 --- a/python/test.py +++ b/python/test.py @@ -19,7 +19,7 @@ from PySide6.QtWidgets import QApplication from version_file import VERSION import resources_rc from settings import settings -from mp_code_event_dict import mp_code_event_dict +from mp_code_parser import mp_code_event_dict from export_trace import export_trace from trace_generator_manager import TraceGeneratorManager from mp_json_io_manager import read_mp_code_file