diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e34e8ce..4d933ba 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: - python-version: [3.9] + python-version: [3.12] steps: diff --git a/pyproject.toml b/pyproject.toml index b0426da..80ee1ca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,3 +22,6 @@ sphinx_mdinclude = "^0.5.1" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" + +[tool.pytest.ini_options] +pythonpath = "arcade-xml-generator" \ No newline at end of file diff --git a/src/GUI.py b/src/GUI.py new file mode 100644 index 0000000..62ef8f4 --- /dev/null +++ b/src/GUI.py @@ -0,0 +1,365 @@ +#This is the file the package is run from. It is still being implemented. It currently interacts read_input_xml. +#In future versions it will save user inputs as dictionary of xml_objects which will then be compiled using compile_xml.py +#Future versions will also work with Potts + +import sys +import os +import xml.etree.ElementTree as ET + +from PyQt5.QtCore import pyqtSignal, Qt +from PyQt5.QtWidgets import ( + QApplication, + QTabWidget, + QVBoxLayout, + QHBoxLayout, + QFormLayout, + QWidget, + QMainWindow, + QLabel, + QComboBox, + QPushButton, + QLineEdit, + QGroupBox, + QToolBar, + QMenu, + QToolButton, + QAction, + QMessageBox +) + +current_script_path = os.path.dirname(os.path.abspath(__file__)) +relative_path_to_XMLObject = os.path.normpath(os.path.join(current_script_path, '../../src/')) +sys.path.append(relative_path_to_XMLObject) +from xml_object import XMLObject +from read_input_xml import ReadInputModuleSpecific +from read_input_xml import ReadInputFunctions + +class Window(QMainWindow): + #Storage of input xml + xml_files_dictionary = {} + selected_xml_parameter_file = {} + parsed_parameter_dictionary = {} + + #Stores user inputs for population id so that + population_id_list = [] + layer_id_list = [] + + #Used to store layout of population tab between additions of new populations + population_tab_layout = None + def __init__(self): + super(Window, self).__init__() + self.initGeneral() + self.initUI() + + def initGeneral(self): + directories = ReadInputFunctions.list_directories() + self.xml_files_dictionary = ReadInputFunctions.find_and_store_xml_files(directories, "parameter") + + def initUI(self): + #This initial UI requests which parameter .xml file you wish to use + self.setWindowTitle('ARCADE Parameter Setup') + central_widget = QWidget(self) + self.setCentralWidget(central_widget) + + self.layout = QVBoxLayout(central_widget) + + # Dropdown menu + self.label = QLabel('Please select which ARCADE module you wish to run:') + self.layout.addWidget(self.label) + xml_file_names_list = list(self.xml_files_dictionary.keys()) + xml_file_names_list.remove("parameter") + self.dropdown = QComboBox(self) + self.dropdown.addItems(xml_file_names_list) + self.layout.addWidget(self.dropdown) + + # Switch button + self.switch_button = QPushButton('Confirm', self) + self.switch_button.clicked.connect(self.select_parameters_screen) + self.layout.addWidget(self.switch_button) + + def select_parameters_screen(self): + # This is the second screen. It displays options based on the selected parameter .xml file + # Save the selected value + self.selected_xml_parameter_file = self.dropdown.currentText() + self.parsed_parameter_dictionary = ReadInputModuleSpecific.read_in_module(ReadInputModuleSpecific, self.selected_xml_parameter_file, self.xml_files_dictionary) + # Clear the current layout + self.clear_layout() + + # Resize + self.resize(1470, 1010) + + # Create the tab widget + ## Possibly should have if statement for patch/potts. Separate tab_ui functions + widget = QWidget() + layout = QVBoxLayout() + tabs = QTabWidget() + tabs.addTab(self.general_tab_ui(), "General") + tabs.addTab(self.series_tab_ui(), "Series") + tabs.addTab(self.module_specific_tab_ui(), "Module Specific") + tabs.addTab(self.population_tab_ui(), "Populations") + tabs.addTab(self.layer_tab_ui(), "Layers") + tabs.addTab(self.action_tab_ui(), "Actions") + tabs.addTab(self.components_tab_ui(), "Components") + layout.addWidget(tabs) + widget.setLayout(layout) + self.setCentralWidget(widget) + self.setupToolbar() + + def clear_layout(self): + # Clear the existing layout by removing all widgets + # Used before switching screens and displaying a new UI + for i in reversed(range(self.layout.count())): + self.layout.itemAt(i).widget().setParent(None) + def setupToolbar(self): + # While the toolbar displays. The functionality is not implemented + # Create a toolbar + toolbar = QToolBar(self) + self.addToolBar(toolbar) + + # Create "Run" button with a dropdown menu + run_button = QToolButton(self) + run_button.setText('Run') + run_menu = QMenu(self) + run_button.setMenu(run_menu) + toolbar.addWidget(run_button) + + # Create "Commands" button with a dropdown menu + commands_button = QToolButton(self) + commands_button.setText('Commands') + commands_menu = QMenu(self) + commands_button.setMenu(commands_menu) + toolbar.addWidget(commands_button) + + # Add actions to the menus + run_action = QAction('Run Action', self) + run_action.triggered.connect(self.run_command) + run_menu.addAction(run_action) + + commands_action = QAction('Commands Action', self) + commands_action.triggered.connect(self.show_commands) + commands_menu.addAction(commands_action) + + def run_command(self): + # NOT IMPLEMENTED + return + + def show_commands(self): + # NOT IMPLEMENTED + commands_text = "Example List of available commands:\n\n1. Command A\n2. Command B\n3. Command C" + QMessageBox.information(self, 'Available Commands', commands_text) + + def general_tab_ui(self): + # Creates a tab for accessing general parameter values + #Set up Container + general_tab = QWidget() + layout = QVBoxLayout() + + #Display parameters from xml + for element in self.parsed_parameter_dictionary["default"]: + nested_layout = QHBoxLayout() # Create a new instance for each iteration + for attribute_name, current_value in element.items(): + if attribute_name == "id" or attribute_name == "description": + label = QLabel(f'{attribute_name}: {current_value}') + nested_layout.addWidget(label) + else: + label = QLabel(f'{attribute_name}: ') + nested_layout.addWidget(label) + + text_field = QLineEdit() + text_field.setText(current_value) + nested_layout.addWidget(text_field) + + layout.addLayout(nested_layout) + + #display everything + general_tab.setLayout(layout) + return general_tab + def series_tab_ui(self): + # Creates a tab for accessing series parameter values + series_tab = QWidget() + layout = QVBoxLayout() + + + #Display parameters from xml + for element in self.parsed_parameter_dictionary["series"]: + nested_layout = QHBoxLayout() # Create a new instance for each iteration + for attribute_name, current_value in element.items(): + if attribute_name == "id" or attribute_name == "description": + label = QLabel(f'{attribute_name}: {current_value}') + nested_layout.addWidget(label) + else: + label = QLabel(f'{attribute_name}: ') + nested_layout.addWidget(label) + + text_field = QLineEdit() + text_field.setText(current_value) + nested_layout.addWidget(text_field) + + layout.addLayout(nested_layout) + + series_tab.setLayout(layout) + return series_tab + def module_specific_tab_ui(self): + # Creates a tab for accessing module specific (i.e., patch vs potts) parameter values + module_specific_tab = QWidget() + layout = QVBoxLayout() + #Display parameters from xml + for element in self.parsed_parameter_dictionary["patch"]: + nested_layout = QHBoxLayout() # Create a new instance for each iteration + for attribute_name, current_value in element.items(): + if attribute_name == "id" or attribute_name == "description": + label = QLabel(f'{attribute_name}: {current_value}') + nested_layout.addWidget(label) + else: + label = QLabel(f'{attribute_name}: ') + nested_layout.addWidget(label) + + text_field = QLineEdit() + text_field.setText(current_value) + nested_layout.addWidget(text_field) + + layout.addLayout(nested_layout) + + module_specific_tab.setLayout(layout) + return module_specific_tab + def population_tab_ui(self): + # Creates a tab for accessing population parameter values + # Can add a new population to see additional sub-population parameters + population_tab = QWidget() + layout = QVBoxLayout() + + nested_layout = QHBoxLayout() + nested_layout.addWidget(QLabel('population: ')) + nested_layout.addWidget(QLabel("id")) + self.population_id_input = QLineEdit(self) + nested_layout.addWidget(self.population_id_input) + nested_layout.addWidget(QLineEdit()) + nested_layout.addWidget(QLabel("init")) + nested_layout.addWidget(QLineEdit()) + nested_layout.addWidget(QLabel("class")) + nested_layout.addWidget(QLineEdit()) + nested_layout.addWidget(QLabel("Valid classes include cancer_stem and tissue")) + layout.addLayout(nested_layout) + self.add_row_button = QPushButton('Add population', self) + self.add_row_button.clicked.connect(self.add_new_population) + layout.addWidget(self.add_row_button) + #population_id_list + population_tab.setLayout(layout) + self.population_tab_layout = layout + return population_tab + def add_new_population(self): + # Displays population parameters once a population is created + # Save the given population IDs for reference by components + population_id = self.population_id_input.text().strip() + + # Create a QGroupBox with the given group box + group_box = QGroupBox(population_id) + group_layout = QVBoxLayout() + # Display parameters from xml + for element in self.parsed_parameter_dictionary["population"]: + nested_layout = QHBoxLayout() # Create a new instance for each iteration + for attribute_name, current_value in element.items(): + if attribute_name == "id" or attribute_name == "description" or attribute_name == "module" or attribute_name == "process": + form_layout = QFormLayout() + label = QLabel(f'{attribute_name}: {current_value}') + form_layout.addWidget(label) + nested_layout.addLayout(form_layout) + else: + form_layout = QFormLayout() + label = QLabel(f'{attribute_name}: ') + form_layout.addWidget(label) + text_field = QLineEdit() + text_field.setText(current_value) + form_layout.addRow(label, text_field) + nested_layout.addLayout(form_layout) + group_layout.addLayout(nested_layout) + + # Set the main layout for the QGroupBox + group_box.setLayout(group_layout) + + # Append the group name to the class variable + Window.population_id_list.append(population_id) + print(f'Group Name List: {Window.population_id_list}') + self.population_tab_layout.addWidget(group_box) + + def layer_tab_ui(self): + # Creates a tab for accessing layer parameter values + layer_tab = QWidget() + layout = QVBoxLayout() + #Display parameters from xml + for element in self.parsed_parameter_dictionary["layer"]: + nested_layout = QHBoxLayout() # Create a new instance for each iteration + for attribute_name, current_value in element.items(): + if attribute_name == "id" or attribute_name == "description": + label = QLabel(f'{attribute_name}: {current_value}') + nested_layout.addWidget(label) + else: + label = QLabel(f'{attribute_name}: ') + nested_layout.addWidget(label) + + text_field = QLineEdit() + text_field.setText(current_value) + nested_layout.addWidget(text_field) + + layout.addLayout(nested_layout) + + layer_tab.setLayout(layout) + return layer_tab + def action_tab_ui(self): + # Creates a tab for accessing action parameter values + action_tab = QWidget() + layout = QVBoxLayout() + #Display parameters from xml + for element in self.parsed_parameter_dictionary["action"]: + nested_layout = QHBoxLayout() # Create a new instance for each iteration + for attribute_name, current_value in element.items(): + if attribute_name == "id" or attribute_name == "description": + label = QLabel(f'{attribute_name}: {current_value}') + nested_layout.addWidget(label) + else: + label = QLabel(f'{attribute_name}: ') + nested_layout.addWidget(label) + + text_field = QLineEdit() + text_field.setText(current_value) + nested_layout.addWidget(text_field) + + layout.addLayout(nested_layout) + + action_tab.setLayout(layout) + return action_tab + def components_tab_ui(self): + # Creates a tab for accessing component parameter values + components_tab = QWidget() + layout = QVBoxLayout() + #Display parameters from xml + for element in self.parsed_parameter_dictionary["component"]: + nested_layout = QHBoxLayout() # Create a new instance for each iteration + for attribute_name, current_value in element.items(): + if attribute_name == "id" or attribute_name == "description": + label = QLabel(f'{attribute_name}: {current_value}') + nested_layout.addWidget(label) + else: + label = QLabel(f'{attribute_name}: ') + nested_layout.addWidget(label) + + text_field = QLineEdit() + text_field.setText(current_value) + nested_layout.addWidget(text_field) + + layout.addLayout(nested_layout) + + components_tab.setLayout(layout) + return components_tab + + +if __name__ == "__main__": + + app = QApplication(sys.argv) + window = Window() + window.show() + sys.exit(app.exec_()) + + + + diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..187eca8 --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,4 @@ +from src.xml_object import XMLObject +from src.read_input_xml import ReadInputFunctions, ReadInputModuleSpecific +from src.compile_xml import ReadOutputFunctions +from src.gui import Window \ No newline at end of file diff --git a/src/compile_xml.py b/src/compile_xml.py new file mode 100644 index 0000000..65563c5 --- /dev/null +++ b/src/compile_xml.py @@ -0,0 +1,38 @@ +#This module is not yet implemented. When it has been fully developed it will be a small module that takes a dictionary of xml_objects +# and organizes them using xml_object methods. The method of arranging the xml_objects will be specific by the string title of the initial .xml parameter file +# A final version of .xml will be saved for setup. Future iterations may include a Linter and a connection to Bash to directly run the setup. + +from logging import raiseExceptions +import tkinter as tk +import xml.etree.ElementTree as ET +import sys +import os + +current_script_path = os.path.dirname(os.path.abspath(__file__)) +relative_path_to_XMLObject = os.path.normpath(os.path.join(current_script_path, '../../src/')) +sys.path.append(relative_path_to_XMLObject) +from xml_object import XMLObject + + +class ReadOutputFunctions(): + def compile(self, dictionary_of_inputs, input_parameter_filename): + #Takes dictionary of inputs, and calls the function associated with the input_parameter_filename + if (input_parameter_filename == "parameter.patch"): + self.user_input_to_patch_setup(dictionary_of_inputs) + elif (input_parameter_filename == "potts.patch"): + self.user_input_to_potts_setup(dictionary_of_inputs) + else: + raise NotImplementedError("This function is not yet implemented.") + return + def user_input_to_patch_setup(self, dictionary_of_inputs): + #Compiles dictionary to create patch setup .xml + #NOT IMPLEMENTED + return + def user_input_to_potts_setup(self, dictionary_of_inputs): + #Compiles dictionary to create potts .xml + #NOT IMPLEMENTED + return + ###### POPULATION LEVEL ###### + def build_a_population(self, population_id_object, population_parameters_object): + + return diff --git a/src/old/edit_xml.py b/src/old/edit_xml.py new file mode 100644 index 0000000..e69de29 diff --git a/src/old/gui.py b/src/old/gui.py new file mode 100644 index 0000000..77a0f4a --- /dev/null +++ b/src/old/gui.py @@ -0,0 +1,77 @@ +import sys +import os +import tkinter as tk +from tkinter import ttk +import datetime +import EditXML +import XMLObject + + +def initiate_GUI(xml_files): + root = tk.Tk() + root.title("XML Editor") + root.geometry("400x300") + #Create a separator + separator = ttk.Separator(root, orient='vertical') + separator.place(relx=0.47, rely=0, relwidth=0.2, relheight=1) + + # Create a frame to hold the XML data + xml_frame = tk.Frame(root) + xml_frame.pack() + + # List to hold XML edit widgets for cleanup + xml_edit_widgets = [] + + # Additional menu with Exit and Save buttons + menu_bar = tk.Menu(root) + root.config(menu=menu_bar) + + file_menu = tk.Menu(menu_bar, tearoff=0) + menu_bar.add_cascade(label="File", menu=file_menu) + + file_menu.add_command(label="Save", command=lambda: save_action()) + file_menu.add_separator() + file_menu.add_command(label="Command Options", command=lambda: command_action(root)) + + # Create a dropdown menu + xml_dropdown = tk.StringVar() + xml_dropdown.set("Select an XML file") + + option_menu = tk.OptionMenu(root, xml_dropdown, *xml_files.keys(), command=lambda selected_key: select_xml_data(selected_key, xml_files, xml_frame)) + option_menu.place(relx=0.5, rely=0.1, relheight=0.10, relwidth=0.4) + + root.mainloop() +def save_action(): + print("Save action executed") + +def command_action(root): + #root.destroy() + print("Checkboxes for commands") + +def select_xml_data(selected_key, XML_trees, xml_frame): + xml_object = XML_trees[selected_key] + display_XMLObject(xml_object, xml_frame) + +def display_XMLObject(xml_object, frame, indent=0): + # Create a button for the current XmlObject + button_text = f"{xml_object.tag} ({len(xml_object.children)})" # Example button text + button = tk.Button(frame, text=button_text, command=lambda: on_button_click(xml_object)) + + # Adjust the row and column parameters + button.grid(row=indent, column=2, sticky="w", padx=indent * 20) # Column is set to 2 + + # Recursively display children + for i, child in enumerate(xml_object.children): + display_XMLObject(child, frame, indent + 1) + +def on_button_click(xml_object): + # Example function to handle button click + print(f"Button clicked for {xml_object.tag}") + +if __name__ == "__main__": + root_directory = 'arcade-xml-generator' + subdirectory = 'xml_files' + directories = EditXML.list_directories() + xml_object = None + xml_files = EditXML.find_and_store_parameter_files_as_XMLObject(directories, "parameter") + initiate_GUI(xml_files) \ No newline at end of file diff --git a/src/old/parameter_object.py b/src/old/parameter_object.py new file mode 100644 index 0000000..45da817 --- /dev/null +++ b/src/old/parameter_object.py @@ -0,0 +1,40 @@ + +import xml.etree.ElementTree as ET +from dataclasses import dataclass, field +from typing import List + +@dataclass(frozen=True, order=True) +class parameter_object: + wrapper_register_or_given_id: str = None + class_wrapper: str = None + subcategory: str = None #e.g. migration module + modified = False + tag: str + attribute_dict: dict = field(default_factory=dict) + description: {} = None #found in some parameter attributes + + +@dataclass(frozen=True, order=True) +class parameters: + set: parameter_object + series: [parameter_object] + model_type: str #no options given in xml + model_type_parameter: [parameter_object] = None #needs wrapper based on file name + + agents: parameter_object(tag = "agents", attribute_dict=None) + agents: parameter_object(tag = "populations", attribute_dict=None) + population: parameter_object #no options given in xml + population_parameters: [parameter_object] + + layers: [parameter_object] #Needs environment, layers, and ID wrapper + actions: [parameter_object] + components: [parameter_object] + + set.name = + +# AUTOADDS EQUIVALENT: +# def __init__(self, name: str, unit_price: float, quantity_on_hand: int = 0): +# self.name = name +# self.unit_price = unit_price +# self.quantity_on_hand = quantity_on_hand + diff --git a/src/read_input_xml.py b/src/read_input_xml.py new file mode 100644 index 0000000..6a1038a --- /dev/null +++ b/src/read_input_xml.py @@ -0,0 +1,108 @@ +import xml.etree.ElementTree as ET +import sys +import os + +current_script_path = os.path.dirname(os.path.abspath(__file__)) +relative_path_to_XMLObject = os.path.normpath(os.path.join(current_script_path, '../../src/')) +sys.path.append(relative_path_to_XMLObject) +from xml_object import XMLObject + + +def element_to_xml_object(element, parent=None): + #helper function for ReadInputFunctions. Takes a element tree and creates an xml_object + xml_object = XMLObject( + tag=element.tag, + attribute_dict=element.attrib, + parent=parent + ) + for child_element in element: + child_xml_object = element_to_xml_object(child_element, parent=xml_object) + xml_object.children.append(child_xml_object) + return xml_object +def list_to_xml_object(element, wrapper): + #helper function for ReadInputFunctions. Takes a list and creates an xml_object + childrenList= [] + for child_element in element: + child_xml_object = XMLObject( + tag=child_element.tag, + attribute_dict=child_element.attrib + ) + childrenList.append(child_xml_object) + root = XMLObject( + tag=wrapper, + children = childrenList + ) + + return root + +#Contains functions that locate and reads in .xml files and outputs dictionaries of element trees. +class ReadInputFunctions(): + def list_directories(): + #Lists all directories starting one level above and going down + directoryList =[] + directoryTuples = os.walk(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) + for x in directoryTuples: + directoryList.append(x[0]) + return directoryList + + def find_and_store_xml_files(directories, keyword): + # Saves all .xml files found in given list of directories that contain the keyword. Currently, the keyword is always "parameter" + xml_files = {} + for directory in directories: + for filename in os.listdir(directory): + if filename.endswith(".xml") and keyword in filename: + file_path = os.path.join(directory, filename) + key = os.path.splitext(os.path.basename(filename))[0]#drops extension + xml_files[key] = (file_path) + return xml_files + + + + def grab_parameters_based_on_prefix(file_name, prefix): + #Grabs ET elements out of .xml file based on prefix + #file_name is a string identifying the .xml file. prefix is a string + tree = ET.parse(file_name) + default_parameters = [] + for elem in tree.iter(): + if elem.tag.startswith(prefix): + default_parameters.append(elem) + return default_parameters + + def load_xml_as_object(file_name): + #Load .xml as an element tree and return the root. Gives a file_name + tree = ET.parse(file_name) + root_element = tree.getroot() + return element_to_xml_object(root_element) + + +class ReadInputModuleSpecific(): + def read_in_patch_parameters(xml_files): + #Reads in from dictionary of xml_files + #Requires parameters.patch.xml and parameters.xml as identified by their keys without .xml + dict_of_parameters = {} + #get default from parameters + dict_of_parameters.update({"default": ReadInputFunctions.grab_parameters_based_on_prefix(xml_files["parameter"], "default")}) + #get series from patch (tag is also default) + dict_of_parameters.update({"series": ReadInputFunctions.grab_parameters_based_on_prefix(xml_files["parameter.patch"], "default")}) + #get patch from patch + dict_of_parameters.update({"patch": ReadInputFunctions.grab_parameters_based_on_prefix(xml_files["parameter.patch"], "patch")}) + #get population from patch + dict_of_parameters.update({"population": ReadInputFunctions.grab_parameters_based_on_prefix(xml_files["parameter.patch"], "population")}) + #get layer from patch + dict_of_parameters.update({"layer": ReadInputFunctions.grab_parameters_based_on_prefix(xml_files["parameter.patch"], "layer")}) + #get action from patch + dict_of_parameters.update({"action": ReadInputFunctions.grab_parameters_based_on_prefix(xml_files["parameter.patch"], "action")}) + #get component from patch + dict_of_parameters.update({"component": ReadInputFunctions.grab_parameters_based_on_prefix(xml_files["parameter.patch"], "component")}) + return dict_of_parameters + + def read_in_potts_parameters(xml_files): + raise NotImplementedError("This function is not yet implemented.") + + def read_in_module(self, module, xml_files): + if module == "parameter.potts": + return self.read_in_potts_parameters(xml_files) + elif module == "parameter.patch": + return self.read_in_patch_parameters(xml_files) + else: + raise NotImplementedError("This is either an invalid parameter file or this function is not yet implemented.") diff --git a/src/xml_object.py b/src/xml_object.py new file mode 100644 index 0000000..af88bf8 --- /dev/null +++ b/src/xml_object.py @@ -0,0 +1,88 @@ +import xml.etree.ElementTree as ET +from dataclasses import dataclass, field +from typing import List +import copy + +@dataclass(order=True) +class XMLObject: + tag: str + attribute_dict: dict = field(default_factory=dict) + children: List['XMLObject'] = field(default_factory=list) + parent: 'XMLObject' = None #Initialized later + default_tag: str = field(init=False) + default_attribute_dict: dict = field(init=False) + + + def __post_init__(self): + # Set parent attribute for each child + for child in self.children: + child.parent = self + # Set default values + self.default_tag = copy.deepcopy(self.tag) + self.default_attribute_dict = copy.deepcopy(self.attribute_dict) + + + def convert_to_element_tree(self): + element = ET.Element(self.tag, self.attribute_dict) + for child in self.children: + child_element = child.convert_to_element_tree() + element.append(child_element) + return element + def save_XML(self, file_name): + tree = ET.ElementTree(self.convert_to_element_tree()) + print(type(tree)) + ET.indent(tree, space="\t", level=0) + tree.write(file_name, encoding="utf-8") + def merge_with_tag(self, donor_xml_object, target_tag): + # Find a node with a matching tag in the target_tag + target_node = self.find_node_with_tag(donor_xml_object, target_tag) + if target_node is not None: + # Append donor_xml_object as a child to the found node + target_node.append(donor_xml_object) + def find_node_with_tag(self, tag): + # Recursive function to find a node with a matching tag + if self.tag == tag: + return self + if self.children == None: + return None + for child in self.children: + node = child.find_node_with_tag(tag) + if node is not None: + return node + return None + def merge_with_id_value(self, donor_xml_object, target_id): + # Find a node with a matching tag in the target_tag + target_node = self.find_node_with_tag(donor_xml_object, target_id) + if target_node is not None: + # Append donor_xml_object as a child to the found node + target_node.append(donor_xml_object) + def find_node_with_id_value(self, id_value): + # Recursive function to find a node with a matching id value + if "id" in self.attribute_dict and self.attribute_dict["id"] == id_value: + return self + if self.children == None: + return None + for child in self.children: + node = child.find_node_with_id_value(id_value) + if node is not None: + return node + return None + def reset_tag(self): + self.tag = self.default_tag + def reset_attribute(self): + self.attribute_dict = self.default_attribute_dict + def reset(self): + self.reset_tag() + self.reset_attribute() + def edit_tag(self, new_tag): + self.tag = new_tag + def edit_attribute(self, key, new_attribute): + if key in self.attribute_dict: + self.attribute_dict[key] = new_attribute + else: + raise KeyError(f"Key '{key}' not found in attribute_dict") + def create_a_population(self, id, parameter_init, parameter_class): + return + + + diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py index e69de29..7c6720c 100644 --- a/tests/unit/__init__.py +++ b/tests/unit/__init__.py @@ -0,0 +1,2 @@ +from unit.test_ObjectToXML import TestObjectCreation +from unit.test_XMLToObject import TestXMLToObject \ No newline at end of file diff --git a/tests/unit/test_compile.py b/tests/unit/test_compile.py new file mode 100644 index 0000000..d398713 --- /dev/null +++ b/tests/unit/test_compile.py @@ -0,0 +1,24 @@ +import unittest +import sys +import os + +current_script_path = os.path.dirname(os.path.abspath(__file__)) +relative_path_to_XMLObject = os.path.normpath(os.path.join(current_script_path, '../../src/')) +sys.path.append(relative_path_to_XMLObject) +from xml_object import XMLObject +from compile_xml import ReadOutputFunctions + +#These tests and the module it tests are not implemented. +class TestCompileSetUpFile(unittest.TestCase): + + #This method will test the pipeline of user input into a xml_object + def test_user_input_to_object(self): + return + + #This method tests the ability to create a .xml patch set up file. + def test_patch_specific(self): + return + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/test_read_input_xml.py b/tests/unit/test_read_input_xml.py new file mode 100644 index 0000000..9c354f0 --- /dev/null +++ b/tests/unit/test_read_input_xml.py @@ -0,0 +1,105 @@ +import sys +import os +import unittest +import xml.etree.ElementTree as ET + +current_script_path = os.path.dirname(os.path.abspath(__file__)) +relative_path_to_XMLObject = os.path.normpath(os.path.join(current_script_path, '../../src/')) +sys.path.append(relative_path_to_XMLObject) +from xml_object import XMLObject +from read_input_xml import ReadInputModuleSpecific +from read_input_xml import ReadInputFunctions + +# Utilized by the tests for quick comparison +def compare_xml_files(file1, file2): + tree1 = ET.parse(file1) + tree2 = ET.parse(file2) + + # Get the root elements + root1 = tree1.getroot() + root2 = tree2.getroot() + + # Compare the XML content + return ET.tostring(root1) == ET.tostring(root2) +def compare_element_lists(list1, list2): + # Check if the lengths are equal + if len(list1) != len(list2): + return False + + # Compare each element in the lists + for elem1, elem2 in zip(list1, list2): + if not compare_elements(elem1, elem2): + return False + + return True + +def compare_elements(elem1, elem2): + # Compare the tag, attributes, and text content of two elements + return ( + elem1.tag == elem2.tag and + elem1.attrib == elem2.attrib and + elem1.text == elem2.text and + compare_element_lists(elem1, elem2) + ) +class TestXMLToObject(unittest.TestCase): + #Tests the read_input_xml.py methods + def setUp(self): + test_files_directory = os.path.dirname(os.path.abspath(__file__)) + os.chdir(test_files_directory) + def test_fine_and_store_parameter_files_as_XMLObject(self): + # Tests that read_input_xml can locate all the parameter .xml files + list_of_directories = ReadInputFunctions.list_directories() + list_of_parameter_files = ReadInputFunctions.find_and_store_xml_files(list_of_directories, "parameter") + tester_parameter_list = ['parameter.patch', 'parameter.potts', 'parameter'] + self.assertEqual(len(list_of_parameter_files), len(tester_parameter_list)) + + def test_convert_xml_to_object(self): + # Tests that .xml file can be converted into a xml_object + list_of_directories = ReadInputFunctions.list_directories() + dict_of_parameter_files = ReadInputFunctions.find_and_store_xml_files(list_of_directories, "") + print("testPrint") + print(dict_of_parameter_files['children_test']) + tree_with_children = ReadInputFunctions.load_xml_as_object(dict_of_parameter_files['children_test']) + XMLObject.save_XML(tree_with_children, 'children_test_converted.xml') + self.assertTrue(compare_xml_files('children_test.xml', 'children_test_converted.xml')) + + + def test_parse_xml_by_tag(self): + # Tests that .xml can be parsed by tag + list_of_directories = ReadInputFunctions.list_directories() + list_of_parameter_files = ReadInputFunctions.find_and_store_xml_files(list_of_directories, "parameter") + parsed_list_of_actions = ReadInputFunctions.grab_parameters_based_on_prefix(list_of_parameter_files['parameter.patch'], 'action') + + list_of_directories = ReadInputFunctions.list_directories() + list_of_parameter_files = ReadInputFunctions.find_and_store_xml_files(list_of_directories, "parameter") + parsed_list_of_patch = ReadInputFunctions.grab_parameters_based_on_prefix(list_of_parameter_files['parameter.patch'], 'layer') + + tree = ET.parse('action_param.xml') + only_list_of_actions = [] + for elem in tree.getroot(): + only_list_of_actions.append(elem) + + self.assertTrue(compare_element_lists(parsed_list_of_actions, only_list_of_actions)) + self.assertFalse(compare_element_lists(parsed_list_of_actions, parsed_list_of_patch)) + + def test_read_in_patch_parameters(self): + #Tests that the patch specific parsing is done correctly + list_of_directories = ReadInputFunctions.list_directories() + list_of_parameter_files = ReadInputFunctions.find_and_store_xml_files(list_of_directories, "parameter") + patch_dict = ReadInputModuleSpecific.read_in_patch_parameters(list_of_parameter_files) + self.assertIn('default', patch_dict, f"default not found in the dictionary") + self.assertIsInstance(patch_dict["default"], list) + self.assertIn("series", patch_dict, f"series not found in the dictionary") + self.assertIsInstance(patch_dict["series"], list) + self.assertIn("population", patch_dict, f"population not found in the dictionary") + self.assertIsInstance(patch_dict["population"], list) + self.assertIn("layer", patch_dict, f"layer not found in the dictionary") + self.assertIsInstance(patch_dict["layer"], list) + self.assertIn("action", patch_dict, f"action not found in the dictionary") + self.assertIsInstance(patch_dict["action"], list) + self.assertIn("component", patch_dict, f"component not found in the dictionary") + self.assertIsInstance(patch_dict["component"], list) + # FUTURE IMPLEMENTATION: Potts test + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/test_xml_object.py b/tests/unit/test_xml_object.py new file mode 100644 index 0000000..5f0e0ea --- /dev/null +++ b/tests/unit/test_xml_object.py @@ -0,0 +1,131 @@ +import unittest +import sys +import os + +current_script_path = os.path.dirname(os.path.abspath(__file__)) +relative_path_to_XMLObject = os.path.normpath(os.path.join(current_script_path, '../../src/')) +sys.path.append(relative_path_to_XMLObject) +from xml_object import XMLObject + +class TestObjectCreation(unittest.TestCase): + #Tests the xml_object initializaiton and associated functions + + def setUp_nochildren_noparent(self): + #Creates an object with no children or parents. + self.population_simple = XMLObject(tag = 'population', attribute_dict = {'id':'C', 'init':'100', 'class':'cancer'}) + def setUp_children(self): + #Creates an object with children and parents. + """ + populations + |-population + |-population parameter + |-population process + |-population process + """ + self.tree_with_children = XMLObject(tag = 'populations') + self.tree_with_children.children.append(XMLObject(tag = 'population', attribute_dict = {'id':'C', 'init':'100', 'class':'cancer'})) + self.tree_with_children.children[0].children.append(XMLObject(tag = 'population.parameter', attribute_dict = {'id':'DIVISION_POTENTIAL', 'value':'3'})) + self.tree_with_children.children[0].children.append(XMLObject(tag = 'population.process', attribute_dict = {'id':'METABOLISM', 'version':'complex'})) + self.tree_with_children.children[0].children.append(XMLObject(tag = 'population.process', attribute_dict = {'id':'SIGNALING', 'version':'complex'}) ) + + def test_to_convert_to_element_tree_simple(self): + #tests the creation of childless tree + self.setUp_nochildren_noparent() + root_element = XMLObject.convert_to_element_tree(self.population_simple) + self.assertEqual(root_element.tag, 'population') + self.assertEqual(root_element.attrib, {'id':'C', 'init':'100', 'class':'cancer'}) + XMLObject.save_XML(self.population_simple, "simpleTest.xml") + + def test_object_save(self): + #tests that the object can be saved as an .xml file + path = os.path.join(os.getcwd(), "saveTest.xml") + if os.path.isfile(path): + os.remove(path) + self.setUp_nochildren_noparent() + XMLObject.save_XML(self.population_simple, path) + self.assertTrue(os.path.isfile(path)) + os.remove(path) + + def test_to_convert_to_element_tree_children(self): + #tests that the object can be turned into a ET from .xml level manipulation + self.setUp_children() + root_element = XMLObject.convert_to_element_tree(self.tree_with_children) + + self.assertEqual(root_element.tag, 'populations') + self.assertEqual(root_element[0].tag, 'population') + self.assertEqual(root_element[0].attrib, {'id':'C', 'init':'100', 'class':'cancer'}) + + children_elements = list(root_element[0]) + + self.assertEqual(children_elements[0].tag, 'population.parameter') + self.assertEqual(children_elements[0].attrib, {'id':'DIVISION_POTENTIAL', 'value':'3'}) + + self.assertEqual(children_elements[1].tag, 'population.process') + self.assertEqual(children_elements[1].attrib, {'id':'METABOLISM', 'version':'complex'}) + + self.assertEqual(children_elements[2].tag, 'population.process') + self.assertEqual(children_elements[2].attrib, {'id':'SIGNALING', 'version':'complex'}) + + XMLObject.save_XML(self.tree_with_children, "children_test.xml") + + def test_edit_attribute(self): + #tests that attributes can be edited + self.setUp_nochildren_noparent() + XMLObject.edit_attribute(self.population_simple,"id", "barry") + self.assertEqual(self.population_simple.attribute_dict["id"], "barry") + + def test_reset_attribute(self): + #tests that attributes can be reset + self.setUp_nochildren_noparent() + XMLObject.edit_attribute(self.population_simple,"id", "barry") + self.population_simple.reset_attribute() + self.assertEqual(self.population_simple.attribute_dict["id"], "C") + + def test_edit_tag(self): + #tests that tags can be edited + self.setUp_nochildren_noparent() + XMLObject.edit_tag(self.population_simple, "barry") + self.assertEqual(self.population_simple.tag, "barry") + def test_reset_tag(self): + #tests that tags can be reset + self.setUp_nochildren_noparent() + XMLObject.edit_tag(self.population_simple, "barry") + self.population_simple.reset_tag() + self.assertEqual(self.population_simple.tag, "population") + def test_reset(self): + #tests that both attributes and tags can be reset at once + self.setUp_nochildren_noparent() + XMLObject.edit_attribute(self.population_simple,"id", "barry") + XMLObject.edit_tag(self.population_simple, "barry") + self.population_simple.reset() + self.assertEqual(self.population_simple.attribute_dict["id"], "C") + self.assertEqual(self.population_simple.tag, "population") + def test_find_node_with_tag(self): + #tests that a node can be identified using a tag + self.setUp_children() + node = XMLObject.find_node_with_tag(self.tree_with_children, "population.parameter") + root_element = XMLObject.convert_to_element_tree(self.tree_with_children) + children_elements = list(root_element[0]) + self.assertEqual(children_elements[0].tag, node.tag) + self.assertEqual(children_elements[0].get("id"), node.attribute_dict["id"]) + + def test_find_node_with_id_value(self): + #tests that a node can be identified using an id + self.setUp_children() + node = XMLObject.find_node_with_id_value(self.tree_with_children, "METABOLISM") + root_element = XMLObject.convert_to_element_tree(self.tree_with_children) + children_elements = list(root_element[0]) + self.assertEqual(children_elements[1].tag, node.tag) + self.assertEqual(children_elements[1].get("id"), node.attribute_dict["id"]) + #Potential future methods: + # def test_merge_with_id_value(self): + # self.setUp_children() + # self.setUp_nochildren_noparent() + + # def test_merge_with_tag(self): + # self.setUp_children() + # self.setUp_nochildren_noparent() + + +if __name__ == "__main__": + unittest.main()