From 8d2fb3c0bbbd36904b2a3327b009472949474dce Mon Sep 17 00:00:00 2001 From: Peter Freedman <104784188+pwfreedm@users.noreply.github.com> Date: Tue, 13 Feb 2024 10:37:16 -0500 Subject: [PATCH 001/144] Update README.md Download points to main instead of develop now that there is something to download in main. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 59708e94..a173d4aa 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ If you don't have python installed (tested on 3.12.2)[ follow the steps here!](h ## Step 1 Download Zip -[Download Zip here!](https://github.com/mucsci-students/2024sp-420-CWorld/archive/refs/heads/develop.zip) +[Download Zip here!](https://github.com/mucsci-students/2024sp-420-CWorld/archive/refs/heads/main.zip) ## Step 2 Extract and Locate @@ -44,4 +44,4 @@ Enter ```help``` within the terminal for a list of commands ## Authors -Adam Glick-Lynch, Ganga Acharya, Marshall Feng, Peter Freedman, Tim Moser \ No newline at end of file +Adam Glick-Lynch, Ganga Acharya, Marshall Feng, Peter Freedman, Tim Moser From 931be47d43137ae77ce14c1a3387a447b4be38f3 Mon Sep 17 00:00:00 2001 From: Peter F Date: Thu, 15 Feb 2024 21:59:35 -0500 Subject: [PATCH 002/144] DO NOT MERGE --- .gitignore | 6 +- dist/2024sp_420_cworld-1.0.0.tar.gz | Bin 0 -> 42506 bytes pyvenv.cfg | 3 + src/Controller/Controller.py | 255 ++++++++++++++++++++++++++++ src/Controller/Input.py | 32 ++++ src/Controller/Output.py | 25 +++ src/Controller/Serializer.py | 83 +++++++++ src/Controller/__init__.py | 5 + src/Controller/pyproject.toml | 74 ++++++++ src/Model/CustomExceptions.py | 220 ++++++++++++++++++++++++ src/Model/Diagram.py | 223 ++++++++++++++++++++++++ src/Model/Entity.py | 95 +++++++++++ src/Model/Help.py | 53 ++++++ src/Model/Relation.py | 80 +++++++++ src/Model/Test.py | 81 +++++++++ src/Model/__init__.py | 5 + src/Model/pyproject.toml | 74 ++++++++ src/View/__init__.py | 0 src/View/pyproject.toml | 74 ++++++++ 19 files changed, 1387 insertions(+), 1 deletion(-) create mode 100644 dist/2024sp_420_cworld-1.0.0.tar.gz create mode 100644 pyvenv.cfg create mode 100644 src/Controller/Controller.py create mode 100644 src/Controller/Input.py create mode 100644 src/Controller/Output.py create mode 100644 src/Controller/Serializer.py create mode 100644 src/Controller/__init__.py create mode 100644 src/Controller/pyproject.toml create mode 100644 src/Model/CustomExceptions.py create mode 100644 src/Model/Diagram.py create mode 100644 src/Model/Entity.py create mode 100644 src/Model/Help.py create mode 100644 src/Model/Relation.py create mode 100644 src/Model/Test.py create mode 100644 src/Model/__init__.py create mode 100644 src/Model/pyproject.toml create mode 100644 src/View/__init__.py create mode 100644 src/View/pyproject.toml diff --git a/.gitignore b/.gitignore index 68ccf3f3..ebe2ab9f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ __pycache__ -*.test \ No newline at end of file +*.test + +Lib/ +Scripts/ +Include/ \ No newline at end of file diff --git a/dist/2024sp_420_cworld-1.0.0.tar.gz b/dist/2024sp_420_cworld-1.0.0.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..98ec448bd0bea99bd585f647398b7a3eaff16309 GIT binary patch literal 42506 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 None: + self._should_quit = False + self._diagram = Diagram() + + #map relating commands to the flags that can be passed to them + #NOTE: These must be synched with the command_function_map based on idx + self._command_flag_map = { + "class" : ["a","d","r"], + "list" : ["a","c","r","d"], + "att" : ["a","d","r"], + "rel" : ["a","d"], + "save" : [""], + "load" : [""], + "exit" : [""], + "quit" : [""], + "help" : [""] + } + + #map relating commands to the names of methods that can be called on them + #NOTE: these must be synched with command_flag_map based on idx + self._command_function_map = { + "class" : ["add_entity","delete_entity","rename_entity"], + "list" : ["list_everything","list_entities","list_relations","list_entity_details"], + "att" : ["add_attribute","delete_attribute","rename_attribute"], + "rel" : ["add_relation","delete_relation"], + "save" : ["save"], + "load" : ["load"], + "exit" : ["quit"], + "quit" : ["quit"], + "help" : ["help_menu"] + } + + def run(self) -> None: + while not self._should_quit: + s = Input.read_line() + + try: + #parse the command + input = self.parse(s) + + #return from input is [function object, arg1,...,argn] + command = input[0] + args = input[1:] + + #execute the command + out = command(*args) + + #write output if it was something + if out != None: + Output.write(out) + + except TypeError as t: + Output.write(CE.InvalidArgCountError(t)) + except ValueError as v: + Output.write(CE.NeedsMoreInput()) + except Exception as e: + Output.write(str(e)) + + def quit(self): + '''Basic Quit Routine. Prompts user to save, where to save, + validates input. + + Returns: + If the name and filepath were valid or user doesn't want to save, returns true + If name is invalid, returns invalid filename exception + If filepath is invalid, returns invalid filepath exception + ''' + self._should_quit = True + while True: + answer = Input.read_line('Would you like to save before quit? [Y]/n: ').strip() + if not answer or answer in ['Y', 'n']: # default or Y/n + break + if answer == 'n': + #user wants to quit without saving + return + else: + answer = Input.read_line('Name of file to save: ') + + if isinstance(self.__check_args([answer]), Exception): + return CE.IOFailedError("Save", "invalid filename") + + self.save(answer) + + def save(self, name: str) -> None: + ''' + Saves the current diagram using a serializer with the given filename + + #### Parameters: + - `name` (str): The name of the file to be saved. + ''' + path = os.path.join(os.path.dirname(__file__), 'save') + if not os.path.exists(path): + os.makedirs(path) + path = os.path.join(path, name + '.json') + Serializer.serialize(diagram=self._diagram, path=path) + + def load(self, name: str) -> None: + ''' + Loads a diagram with the given filename using a deserializer. + + #### Parameters: + - `path` (str): The name of the file to be loaded. + ''' + path = os.path.join(os.path.dirname(__file__), 'save') + if not os.path.exists(path): + os.makedirs(path) + path = os.path.join(path, name + '.json') + loadedDiagram = Diagram() + Serializer.deserialize(diagram=loadedDiagram, path=path) + self._diagram = loadedDiagram + + def parse (self, input:str) -> list: + '''Parses a line of user input. + + Args: + input(str) - the line to be parsed + + Return: + With proper input: a list in the form [function, arg1,...,argN] + With invalid name: CustomExceptions.InvalidArgumentError + With invalid flag: CustomExceptions.InvalidFlagError + With invalid command: CustomExceptions.CommandNotFoundError + ''' + #guarding no input saves resources + if not input: + raise CE.NoInputError() + + #actual input to be parsed, split on spaces + bits = input.split() + + #Get the command that will be run + command_str = "" + #list slicing generates an empty list instead of an IndexError + command_str = self.__find_function(bits[0:1], bits[1:2]) + + #Get the args that will be passed to that command + args = [] + if not str(bits[1:2]).__contains__("-"): + args = self.__check_args(bits[1:]) + else: + args = self.__check_args(bits[2:]) + + #Get the class the command is in + command_class = self.__find_class(command_str) + + #go from knowing which class to having a specific instance + #of the class that the method needs to be called on + obj = self + if command_class == Diagram: + obj = self._diagram + elif command_class == Entity: + #if no args were provided, no entity can be found. Generate an error about invalid args + if not args: + raise CE.NoArgsGivenError() + + #if the method is in entity, get entity that needs to be changed + #pop the first element of args because it is the entity name, not a method param + obj = self._diagram.get_entity(args.pop(0)) + elif command_class == Help: + obj = Help + + #build and return the callable + args + return [getattr(obj, command_str)] + args + + + def __check_args(self, args:list): + '''Given a list of args, checks to make sure each one is valid. + Valid is defined as alphanumeric. + + Args: + args(list): a list of strings to be checked + + Return: + CustomExceptions.InvalidArgumentError if an argument provided is invalid + The list of args provided if all args are valid + ''' + for arg in args: + if not(arg.isalnum()): + raise CE.InvalidArgumentError(arg) + return args + + def __find_function(self, command:list, flag:list): + '''Finds the name of the function that should be called + + Args: + command - the command that was given to the parser + flag - the flag that was given to the parser + + Raises: + CustomExceptions.CommandNotFoundError if the command entered is + invalid + CustomExceptions.InvalidFlagError if the flag entered is invalid + + Returns: + The name of the function that needs to be called + ''' + #convert the params to strings + command = str(command[0]) + flag = str(flag[0]) if len(flag) > 0 else "" + + #check if the list of keys in the commmand flag map contains the given command + command_list = list(self._command_flag_map.keys()) + + valid_command = command_list.__contains__(command) + if not valid_command: + raise CE.CommandNotFoundError(command) + + #pull the list of flags for the validated command + flag_list = self._command_flag_map[command] + + #Make sure that the flag is a flag (preceded with -) + #some commands are just "command arg" so this needs to be checked + prepped_flag = "" + valid_flag = True + if flag.__contains__("-"): + prepped_flag = flag.lstrip("-") + valid_flag = flag_list.__contains__(prepped_flag) + + if not valid_flag: + raise CE.InvalidFlagError(flag, command) + + #compiling the correct location to index into the function map + flag_index = flag_list.index(prepped_flag) + flags = self._command_function_map.get(command) + return flags[flag_index] + + def __find_class(self, function:str): + '''Takes a function and locates the class that it exists in + + Args: + function - the function to locate in a class + + Returns: + the class the function originates in''' + classes = [Diagram, Entity, Relation, Help] + for cl in classes: + if hasattr(cl, function): + return cl + diff --git a/src/Controller/Input.py b/src/Controller/Input.py new file mode 100644 index 00000000..6cb1ce5b --- /dev/null +++ b/src/Controller/Input.py @@ -0,0 +1,32 @@ +from CustomExceptions import CustomExceptions as CE + +def read_line(s='Command: ') -> str: + """ + Print a message to cmd and get a line of input + + ## Parameters: + - `s` (str): The message to print before asking for input + + ## Returns: + - (str): A line of input + """ + return input(s) + +def read_file(path: str) -> str: + """ + Reads the contents of a file and returns the content as a string. + + #### Parameters: + - `path` (str): The path to the file to be read. + + #### Returns: + - (str): The content of the file as a string. + + #### Raises: + - (CustomExceptions.ReadFileError): If failed to read the file + """ + try: + with open(path, 'r') as file: + return file.read() + except Exception: + raise CE.ReadFileError(filepath=path) diff --git a/src/Controller/Output.py b/src/Controller/Output.py new file mode 100644 index 00000000..c6599752 --- /dev/null +++ b/src/Controller/Output.py @@ -0,0 +1,25 @@ +from CustomExceptions import CustomExceptions as CE + +def write(s: str) -> None: + print(s) + +def write_file(path: str, content: str) -> None: + ''' + Writes the specified content to a file at the given path. + + # Parameters: + - `path` (str): The path to the file to be written. + - `content` (str): The content to be written to the file. + + # Returns: + - None + + # Raises: + - `FileNotFoundError`: If the specified file is not found. + - `IOError`: If there is an issue writing to the file. + - Other exceptions: Any other exceptions that may occur during the file writing process. + ''' + try: + open(path, 'w').write(content) + except Exception: + raise CE.WriteFileError(path) \ No newline at end of file diff --git a/src/Controller/Serializer.py b/src/Controller/Serializer.py new file mode 100644 index 00000000..69679723 --- /dev/null +++ b/src/Controller/Serializer.py @@ -0,0 +1,83 @@ +import json + +class CustomJSONEncoder(json.JSONEncoder): + ''' + a custom JSON encoder that returns a list when it encounters a set + ''' + def default(self, obj): + ''' + The `default` method is a custom implementation within a class that inherits from the `json.JSONEncoder` class in Python. + It enhances the JSON serialization process by providing special handling for sets. + ''' + if isinstance(obj, set): + return list(obj) + return json.JSONEncoder.default(self, obj) + +import Input +import Output +from Diagram import Diagram +from Entity import Entity +from Relation import Relation +from CustomExceptions import CustomExceptions as CE + +def serialize(diagram: Diagram, path: str) -> None: + ''' + Serialize a diagram's entities and relations to a JSON file. + + #### Parameters: + - `diagram` (Diagram): The diagram object containing entities and relations to be serialized. + - `path` (str): The file path where the JSON file will be saved. + ''' + entities = {name: vars(obj) for name, obj in diagram._entities.items()} + relations = [] + for x in diagram._relations: + properties = vars(x) + for property_name, property_val in properties.items(): + if isinstance(property_val, Entity): + properties[property_name] = property_val.get_name() + relations.append(properties) + try: + content = json.dumps(obj={'entities': entities, 'relations': relations}, cls=CustomJSONEncoder) + except Exception: + raise CE.JsonEncodeError(filepath=path) + Output.write_file(path=path, content=content) + +def deserialize(diagram: Diagram, path: str) -> None: + ''' + Deserialize a diagram from a JSON file and populate its entities and relations. + + #### Parameters: + - `diagram` (Diagram): The diagram object to populate with deserialized data. + - `path` (str): The file path of the JSON file to deserialize. + + #### Raises: + - (CustomExceptions.JsonDecodeError): If failed to decode the file + - (CustomExceptions.SavedDataError): If file data is not consistent with the Diagram + ''' + content = Input.read_file(path) + try: + diagram_attributes = json.loads(content) + except Exception: + raise CE.JsonDecodeError(filepath=path) + try: + for attr_name, attr_obj in diagram_attributes.items(): + if attr_name == 'entities': + for name, properties in attr_obj.items(): + entity = Entity() + for property_name, property_val in properties.items(): + if isinstance(getattr(entity, property_name), set): # Because custom encoder save set as list + property_val = set(property_val) + setattr(entity, property_name, property_val) + diagram._entities[name] = entity + elif attr_name == 'relations': + for properties in attr_obj: + relation = Relation() + for property_name, property_val in properties.items(): + if isinstance(getattr(relation, property_name), Entity): + property_val = diagram._entities[property_val] + if isinstance(getattr(relation, property_name), set): # Because custom encoder save set as list + property_val = set(property_val) + setattr(relation, property_name, property_val) + diagram._relations.append(relation) + except Exception: + raise CE.SavedDataError(filepath=path) \ No newline at end of file diff --git a/src/Controller/__init__.py b/src/Controller/__init__.py new file mode 100644 index 00000000..b90a7a4a --- /dev/null +++ b/src/Controller/__init__.py @@ -0,0 +1,5 @@ +from .Controller import Controller +from .Input import Input +from .Output import Output +from .Serializer import serialize +from .Serializer import deserialize \ No newline at end of file diff --git a/src/Controller/pyproject.toml b/src/Controller/pyproject.toml new file mode 100644 index 00000000..64f41ade --- /dev/null +++ b/src/Controller/pyproject.toml @@ -0,0 +1,74 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "controller" +dynamic = ["version"] +description = 'Controls the data flow between Model and View.' +readme = "README.md" +requires-python = ">=3.8" +license = "MIT" +keywords = [] + +classifiers = [ + "Development Status :: 4 - Beta", + "Programming Language :: Python", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] +dependencies = [] + +[tool.hatch.version] +path = "src/controller/__about__.py" + +[tool.hatch.envs.default] +dependencies = [ + "coverage[toml]>=6.5", + "pytest", +] +[tool.hatch.envs.default.scripts] +test = "pytest {args:tests}" +test-cov = "coverage run -m pytest {args:tests}" +cov-report = [ + "- coverage combine", + "coverage report", +] +cov = [ + "test-cov", + "cov-report", +] + +[[tool.hatch.envs.all.matrix]] +python = ["3.8", "3.9", "3.10", "3.11", "3.12"] + +[tool.hatch.envs.types] +dependencies = [ + "mypy>=1.0.0", +] +[tool.hatch.envs.types.scripts] +check = "mypy --install-types --non-interactive {args:src/controller tests}" + +[tool.coverage.run] +source_pkgs = ["controller", "tests"] +branch = true +parallel = true +omit = [ + "src/controller/__about__.py", +] + +[tool.coverage.paths] +controller = ["src/controller", "*/controller/src/controller"] +tests = ["tests", "*/controller/tests"] + +[tool.coverage.report] +exclude_lines = [ + "no cov", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", +] diff --git a/src/Model/CustomExceptions.py b/src/Model/CustomExceptions.py new file mode 100644 index 00000000..120fc1d0 --- /dev/null +++ b/src/Model/CustomExceptions.py @@ -0,0 +1,220 @@ +import re #for argc errors +class CustomExceptions: + class Error(Exception): + """Base class for other exceptions.""" + pass + + #===============================================================================# + #Entity Exceptions + #===============================================================================# + class EntityExistsError(Error): + """Exception raised when an entity with a given name already exists. + + Args: + name (str): The name of the existing entity. + """ + def __init__(self, name) -> None: + super().__init__(f"Entity with name '{name}' already exists.") + + class EntityNotFoundError(Error): + """ + Exception raised when an entity with the given name is not found. + + Args: + name (str): The name of the entity not found. + """ + def __init__(self, name) -> None: + super().__init__(f"Entity with name '{name}' does not exist.") + + #===============================================================================# + #Attribute Exceptions + #===============================================================================# + class AttributeExistsError(Error): + """Exception raised when an attribute with a given name already exists. + + Args: + attr (str): The name of the existing attribute. + """ + def __init__(self, attr) -> None: + super().__init__(f"Attribute with name '{attr}' already exists.") + + class AttributeNotFoundError(Error): + """Exception raised when an attribute with a given name is not found. + + Args: + attr (str): The name of the attribute not found. + """ + def __init__(self, attr) -> None: + super().__init__(f"Attribute with name '{attr}' does not exist.") + + #===============================================================================# + #Relation Exceptions + #===============================================================================# + class RelationExistsError(Error): + """ + Exception raised when the relation being added already exists. + + Args: + source (Entity): The source of the relation that was being added. + destination (Entity): The destination of the relation that was + being added. + + """ + def __init__(self, source, destination): + super().__init__(f"Relation between '{source} -> {destination}' already exists.") + + class RelationDoesNotExistError(Error): + """ + Exception raised when the relation being deleted does not exist. + + Args: + source (Entity): The source of the relation that was being deleted. + destination (Entity): The destination of the relation that was + being deleted. + + """ + def __init__(self, source, destination): + super().__init__(f"Relation between '{source} -> {destination}' does not exist.") + + #===============================================================================# + #Parser/Controller Exceptions + #===============================================================================# + class InvalidArgumentError(Error): + """ + Exception raised when an input argument is not valid. + + Args: + name (str): The name of the argument that was not found. + """ + def __init__(self, name) -> None: + super().__init__(f"Argument '{name}' is not alphanumeric.") + + class InvalidFlagError(Error): + """ + Exception raised when an input flag is not valid. + + Args: + flag (str): The name of the flag that was not found. + command (str): The name of the command that was called with the invalid flag. + """ + def __init__(self, flag, command) -> None: + super().__init__(f"Command '{command}' has no flag '{flag}'.") + + class NoEntitySelected(Error): + """ + Exception raised when an input argument is not valid. + + Args: + flag (str): The name of the argument that was not found. + command (str): The name of the command that was called with the invalid flag. + """ + def __init__(self) -> None: + super().__init__(f"No class selected. Use 'class -s name' to select a class.") + + class CommandNotFoundError(Error): + """Exception raised when an invalid command is entered""" + + def __init__(self, name) -> None: + super().__init__(f"Command '{name}' does not exist. Try again or type 'help' for help.") + + class InvalidArgCountError(Error): + ''' + Exception raised when a user enters too many or too few arguments to the command they want to call. + + NOTE: This method really just catches the TypeError python generates and makes the message mroe readable. + + Args: + error - the TypeError that needs to be replaced + ''' + def __init__(self, error): + #matching the different syntaxes of TypeErrors + match = re.search(r".*(\d+).*(\d+).*", str(error)) + if match == None: + match = re.search(r".*(\d+).*", str(error)) + super().__init__(f"{match.group(1)} too few arguments given.") + else: + super().__init__(f"Expected {match.group(1)} arguments, but {match.group(2)} were given.") + + class NoArgsGivenError(Error): + ''' + Exception raised when a user gives no args to a command that requires one to be parsed + ''' + def __init__(self): + super().__init__(f"This command requires additional input. Type 'help' for command usage.") + + class NoInputError(Error): + ''' + Exception raised when no input is given and enter is hit + ''' + def __init__(self): + super().__init__(f"") + + class NeedsMoreInput(Error): + ''' + Exception raised when user gives only a command name + ''' + def __init__(self): + super().__init__(f"This command requires more input. Please try again or type 'help' for command usage.") + #===============================================================================# + #I/O Exceptions + #===============================================================================# + + class IOFailedError(Error): + '''Exception raised when an I/O Operation fails (saving or loading) + + Args: + opname (str): the name of the operation that failed + reason (str): the reason that opname failed + ''' + def __init__(self, opname, reason) -> None: + super().__init__(f"{opname} failed due to {reason}. Please try again.") + + class ReadFileError(Error): + '''Exception raised when failed to read a file + + #### Args: + `filepath` (str): the path of the file to read + ''' + def __init__(self, filepath: str) -> None: + super().__init__('Can not read file: "{}".'.format(filepath)) + + class WriteFileError(Error): + '''Exception raised when failed to write a file + + #### Args: + `filepath` (str): the path of the file to write + ''' + def __init__(self, filepath: str) -> None: + super().__init__('Can not write file: "{}".'.format(filepath)) + + #=============================================================================== + #Serializer Exceptions + #=============================================================================== + + class JsonDecodeError(Error): + '''Exception raised when failed to decode a Json file + + #### Args: + `filepath` (str): the path of the Json file to decode + ''' + def __init__(self, filepath: str) -> None: + super().__init__('Can not decode .json file: "{}".'.format(filepath)) + + class JsonEncodeError(Error): + '''Exception raised when failed to encode a Json file + + #### Args: + `filepath` (str): the path of the Json file to encode + ''' + def __init__(self, filepath: str) -> None: + super().__init__('Can not encode .json file: "{}".'.format(filepath)) + + class SavedDataError(Error): + '''Exception raised when the saved data is not consistent with the Diagram. + + #### Args: + `filepath` (str): the path of the save file + ''' + def __init__(self, filepath: str) -> None: + fmt = 'Failed to load save data: "{}".(Data in this save is no longer valid)' + super().__init__(fmt.format(filepath)) \ No newline at end of file diff --git a/src/Model/Diagram.py b/src/Model/Diagram.py new file mode 100644 index 00000000..9f913738 --- /dev/null +++ b/src/Model/Diagram.py @@ -0,0 +1,223 @@ +from math import e +from Entity import Entity +from Relation import Relation +from CustomExceptions import CustomExceptions + +class Diagram: + def __init__(self) -> None: + self._entities = {} + self._relations = [] + + def add_entity(self, name: str): + """ + Adds an entity with the given name to the Diagram. + + Args: + name (str): The name of the entity to add. + + Raises: + CustomExceptions.EntityExistsError: If an entity + with the same name already exists. + + Returns: + None + """ + if name in self._entities: + raise CustomExceptions.EntityExistsError(name) + else: + self._entities[name] = Entity(name) + + def get_entity(self, name: str): + """ + Retrieves an entity from the diagram if it exists. + + Args: + name (str): The name of the entity to be retrieved. + + Raises: + CustomExceptions.EntityNotFoundError if the entity requested does not exist + + Returns: + Entity: If the entity exists. + None: If the entity does not exist. + """ + entity = self._entities.get(name, None) + if entity is None: + raise CustomExceptions.EntityNotFoundError(name) + else: + return entity + + + def delete_entity(self, name: str): + """ + Delete a given entity from the diagram. + + Args: + name (str): The name of the entity to be deleted. + + Raises: + CustomExceptions.EntityNotFoundError: If an entity to be deleted + does not exist. + + Returns: + None + """ + if name not in self._entities: + raise CustomExceptions.EntityNotFoundError(name) + + # Check for relations involving the entity and remove them + else: + relations_to_remove = [] + for rel in self._relations: + if rel.contains(self._entities[name]): + relations_to_remove.append(rel) + for rel in relations_to_remove: + self._relations.remove(rel) + + # Remove the entity from the dictionary + del self._entities[name] + + + def rename_entity(self, old_name: str, new_name: str): + """ + Rename a given entity with a new name. + + Args: + old_name (str): The current name of the entity to be renamed. + new_name (str): The new name for the entity. + + Raises: + CustomExceptions.EntityNotFoundError: If an entity with the old name + does not exist. + CustomExceptions.EntityExistsError: If an entity with the new name + already exists. + + Returns: + None + """ + if old_name not in self._entities: + raise CustomExceptions.EntityNotFoundError(old_name) + elif new_name in self._entities: + raise CustomExceptions.EntityExistsError(new_name) + # Update key + else: + entity = self._entities[old_name] + entity.set_name(new_name) + self._entities[new_name] = self._entities.pop(old_name) + + def list_everything(self): + """ + Returns a representation of the entire diagram. + + Returns: + str: A templated representation of all entities, their attributes, + and their relations. + """ + result = "" + for entity in self._entities.values(): + result += self.list_entity_details(entity.get_name()) + "\n" + return result + + def list_entity_details(self, entity_name): + """ + Returns the attributes and relations of the entity. + + Args: + entity_name (str): The name of the entity to get details of. + + Raises: + None + + Returns: + str: A templated string containing the attributes and relations. + """ + if not self._entities.__contains__(entity_name): + raise CustomExceptions.EntityNotFoundError(entity_name) + else: + entity = self._entities[entity_name] + att = entity._attributes + rels = [rel for rel in self._relations if rel.contains(entity)] + result ="\n" + entity_name + "'s Attributes:\n" + att_string = ', '.join(att) + result2 = entity_name + "'s Relations:\n" + rel_string = ', '.join(str(rel) for rel in rels) + return result + att_string + "\n" + result2 + rel_string + + def list_entities(self): + """ + Returns the entities in the relation. + + Returns: + str: String containing names of all existing entities. + """ + entity_names = list(self._entities.keys()) + return '\n' + ', '.join(entity_names) + + def list_relations(self): + """ + Lists all existing relations as a string. + + Returns: + str: A string representation of all existing relations. + """ + relations_list = [] + for rel in self._relations: + relations_list.append(str(rel)) + return '\n'.join(relations_list) + + + def add_relation(self, source, destination): + """ + Adds a relation between two Entities. + + Args: + source (str): The name of the source entity. + destination (str): The name of the destination entity. + + Raises: + CustomExceptions.EntityNotFoundError: If either the source or the + destination entity are not found. + CustomExceptions.RelationExistsError: If a relation already exists + between the source and destination entities. + """ + # Check for valid source and destination + if source not in self._entities: + raise CustomExceptions.EntityNotFoundError(source) + elif destination not in self._entities: + raise CustomExceptions.EntityNotFoundError(destination) + # Check for duplicate relationship containing same source and destination + else: + for rel in self._relations: + if rel.get_source() == self._entities[source] and rel.get_destination() == self._entities[destination]: + raise CustomExceptions.RelationExistsError(source, destination) + # Pass entity objects to relation and add relation to list of existing relations + relationship = Relation(self._entities[source], self._entities[destination]) + self._relations.append(relationship) + + def delete_relation(self, source, destination): + """ + Deletes a relation between two Entities. + + Args: + source (str): The name of the source entity. + destination (str): The name of the destination entity. + + Raises: + CustomExceptions.EntityNotFoundError: If either the source or the + destination entity is not found. + CustomExceptions.RelationDoesNotExistError: If a relation does not + exist between the source and destination entities. + """ + # Check for valid source and destination + if source not in self._entities: + raise CustomExceptions.EntityNotFoundError(source) + elif destination not in self._entities: + raise CustomExceptions.EntityNotFoundError(destination) + # Look for matching relation to delete + else: + for i, rel in enumerate(self._relations): + if rel.get_source() == self._entities[source] and rel.get_destination() == self._entities[destination]: + del self._relations[i] + return + raise CustomExceptions.RelationDoesNotExistError(source, destination) + diff --git a/src/Model/Entity.py b/src/Model/Entity.py new file mode 100644 index 00000000..3aba76ea --- /dev/null +++ b/src/Model/Entity.py @@ -0,0 +1,95 @@ +from CustomExceptions import CustomExceptions + +class Entity: + def __init__(self, name:str='') -> None: + """ + Constructs a new Entity object. + + Args: + name (str): The name of the entity. + """ + self.set_name(name) + self._attributes = set() + + def get_name(self) -> str: + ''' + Returns the name of the entity. + + # Returns: + (str): The name of the entity + ''' + return self._name + + def set_name(self, name: str) -> None: + """ + Update entity name. + + Args: + name (str): The new name for the entity. + """ + self._name = name + + def add_attribute(self, attr:str) -> None: + """ + Adds a new attribute to the to the entity. + + Args: + attr (str): The attribute name to be added to the entity. + + Raises: + CustomExceptions.AttributeExistsError: If the attribute already + exists in the Entity. + """ + if attr in self._attributes: + raise CustomExceptions.AttributeExistsError(attr) + else: + self._attributes.add(attr) + + def delete_attribute(self, attr: str) -> None: + """ + Deletes an attribute from this entity if it exists. + + Args: + attr (str): The name of the attribute to be deleted from the entity. + + Raises: + CustomExceptions.AttributeNotFoundError: If the specified attribute + is not found in the entity's attributes. + """ + if attr not in self._attributes: + raise CustomExceptions.AttributeNotFoundError(attr) + else: + self._attributes.remove(attr) + + def rename_attribute(self, old_attribute: str, new_attribute: str) -> None: + """ + Renames an attribute from its old name to a new name + + Args: + oldAttribute (str): The current name of the attribute. + newAttribute (str): The new name for the attribute + + Raises: + CustomExceptions.AttributeNotFoundError: If the old attribute does + not exist in the entity. + CustomExceptions.AttributeExistsError: If the new name is already + used for another attribute in this entity. + """ + if old_attribute not in self._attributes: + raise CustomExceptions.AttributeNotFoundError(old_attribute) + + elif new_attribute in self._attributes: + raise CustomExceptions.AttributeExistsError(new_attribute) + # Remove the old attribute and add the new attribute name + else: + self._attributes.remove(old_attribute) + self._attributes.add(new_attribute) + + def __str__(self) -> str: + """ + Returns the string representation of the Entity object (its name). + + Returns: + str: The name of the entity. + """ + return self._name diff --git a/src/Model/Help.py b/src/Model/Help.py new file mode 100644 index 00000000..babcbfdc --- /dev/null +++ b/src/Model/Help.py @@ -0,0 +1,53 @@ +'''Application help menu, to be called when user asks for help.''' +def help_menu(): + """ + Returns a string that contains the menu. + + Args: + None. + + Raises: + None. + + Returns: + str(): The help list to be printed. + """ + menu = ( + #A general description + "\nHelp menu: For the best view, resize your window so that this message and the bar at the end are on one line. |\n\n" + "Below are the commands you can call and an explanation of what each does. Anything inside single quotes is decided\n" + "by you! Enter the command, replacing anything in the single quotes, and the quotes themselves, with the name you\n" + "want to use.\n\n" + "A valid name is made up of any combination of letters and numbers.\n\n" + #Class Commands + "Class Commands: \n\t" + "class -a 'name' - adds a class with name 'name'. Cannot add classes with duplicate or invalid names\n\t" + "class -d 'name' - deletes a class with name 'name'\n\t" + "class -r 'old' 'new' - renames class 'old' to 'new'. Cannot rename classes to duplicate or invalid names\n" + #Attribute Commands + "Attribute Commands: \n\t" + "att -a class 'name' - adds an attribute with name 'name' to class 'class'\n\t" + "att -d class 'name' - deletes an attribute with name 'name' from class 'class' if one exists\n\t" + "att -r class 'old' 'new' - renames an attribute from name 'old' to name 'new' in class 'class'\n" + #Relation Commands + "Relation Commands:\n\t" + "rel -a 'src' 'dest' - adds a relationship between class 'src' and class 'dest' assuming both are valid\n\t" + "rel -d 'src' 'dest' - deletes a relationship between class 'src' and class 'dest' if one exists\n" + #List Commands + "List Flags: \n\t" + "list -a - list all classes and their contents in the UML Diagram\n\t" + "list -c - list all classes in the UML Diagram\n\t" + "list -r - list all relationships in the UML Diagram\n\t" + "list -d 'name' - list all contents of class 'name'\n" + #Save Commands + "Save Flags: \n\t" + "save 'name' - saves the UML Diagram as a JSON file with name 'name'\n" + #Load Commands + "Load Flags: \n\t" + "load 'name' - loads the file with name 'name.json' if one exists.\n\n" + #Exit Commands + "Exit Commands: \n\t" + "exit - terminates the program.\n\t" + "quit - terminates the program.\n" + ) + return menu diff --git a/src/Model/Relation.py b/src/Model/Relation.py new file mode 100644 index 00000000..4cb638c5 --- /dev/null +++ b/src/Model/Relation.py @@ -0,0 +1,80 @@ +from Entity import Entity + +class Relation: + def __init__(self, source=Entity(), destination=Entity()): + """ + Creates a relation between a source entity to a destination entity. + + Args: + source (Entity): The entity at the start of the relation. + destination (Entity): The entity at the end of the relation. + + Raises: + None. + + Returns: + None. + """ + self._source = source + self._destination = destination + + def get_source(self): + """ + Returns the source entity of the the relation. + + Args: + None. + + Raises: + None. + + Returns: + source (Entity): The source entity of the relation. + """ + return self._source + + def get_destination(self): + """ + Returns the destination entity of the relation. + + Args: + None. + + Raises: + None. + + Returns: + destination (Entity): The destination entity of the relation. + """ + return self._destination + + def contains(self, entity: Entity): + """ + Checks if a given entity is part of the relation. + + Args: + entity (Entity): The entity to be checked. + + Returns: + bool: Returns True if the entity is the source or the destination + of the relation. Returns False if the entity is not in the relation. + """ + if entity == self._source or entity == self._destination: + return True + else: + return False + + def __str__(self): + """ + Returns a string representation of a relation. + + Args: + None. + + Raises: + None. + + Returns: + str: A string representation of the relation. + """ + return f'{self._source} -> {self._destination}' diff --git a/src/Model/Test.py b/src/Model/Test.py new file mode 100644 index 00000000..37191896 --- /dev/null +++ b/src/Model/Test.py @@ -0,0 +1,81 @@ +class Test: + def __init__(self, name:str, func): + self.func = func + self.name = name + + + def exec(self, detail:str, expected, *args): + ''' + Executes self.func, passing in all args in the order provided + + Args: + detail - a little extra info about what case is being tested + expected - the expected output of this test. Can take any form with a defined __str__ + *args - a variadic list of all arguments that self.func needs to run + + Return: + A string in the form "self.name detail - passed" if the test was passed + A string in the form "self.name detail - expected {} actual {}" if the test was failed + + ''' + try: + output = self.func(*args) + except Exception as e: + return self.__createOutput(detail, str(expected), str(e)) + + + return self.__createOutput(detail, str(expected), str(output)) + + + def checkUpdate(self, detail:str, expected, searchLoc, *args): + ''' + Executes self.func, passing in all args in the order provided + + Args: + detail - a little extra info about what case is being tested + expected - the expected output of this test. Can take any form with a defined __str__ + searchLoc - the function to call to search for the expected output + *args - a variadic list of all arguments that self.func needs to run + + Return: + A string in the form "self.name detail - passed" if the test was passed + A string in the form "self.name detail - expected {} actual {}" if the test was failed + + ''' + try: + self.func(*args) + except Exception as e: + return self.__createOutput(detail, str(expected), str(e)) + + return self.__createOutput(detail, str(expected), str(searchLoc())) + + '''Private helper that takes an expected and actual output, then checks and formats them. + param detail - a short message about what case is being tested specifically + param expected - the expected outcome to compare to + param actual - the actual outcome of the test + returns a string in the form "self.name detail - passed" if the test was passed + returns a string in the form "self.name detail - expected {} actual {}" if the test was failed + ''' + def __createOutput(self, detail:str, expected:str, actual:str): + ''' + Private helper to create the output string returned from a call to any method that runs a test. + + Args: + detail - a little extra info about what case is being tested + expected - the expected output of this test, as a string + actual - the actual output of this test, as a string + + Return: + A string in the form "self.name detail - passed" if the test was passed + A string in the form "self.name detail - expected {} actual {}" if the test was failed + + ''' + output = self.name + ": " + detail + " - " + + if(expected == actual): + return output + "passed" + else: + return output + "\n\tExpected: " + expected + "\n\tActual: " + actual + + + \ No newline at end of file diff --git a/src/Model/__init__.py b/src/Model/__init__.py new file mode 100644 index 00000000..43c86f9f --- /dev/null +++ b/src/Model/__init__.py @@ -0,0 +1,5 @@ +from .CustomExceptions import CustomExceptions +from .Diagram import Diagram +from .Entity import Entity +from .Help import help_menu +from .Relation import Relation \ No newline at end of file diff --git a/src/Model/pyproject.toml b/src/Model/pyproject.toml new file mode 100644 index 00000000..e9f7f43f --- /dev/null +++ b/src/Model/pyproject.toml @@ -0,0 +1,74 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "model" +dynamic = ["version"] +description = 'The format and condition of the data in the UML Diagram.' +readme = "README.md" +requires-python = ">=3.8" +license = "MIT" +keywords = [] + +classifiers = [ + "Development Status :: 4 - Beta", + "Programming Language :: Python", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] +dependencies = [] + +[tool.hatch.version] +path = "src/model/__about__.py" + +[tool.hatch.envs.default] +dependencies = [ + "coverage[toml]>=6.5", + "pytest", +] +[tool.hatch.envs.default.scripts] +test = "pytest {args:tests}" +test-cov = "coverage run -m pytest {args:tests}" +cov-report = [ + "- coverage combine", + "coverage report", +] +cov = [ + "test-cov", + "cov-report", +] + +[[tool.hatch.envs.all.matrix]] +python = ["3.8", "3.9", "3.10", "3.11", "3.12"] + +[tool.hatch.envs.types] +dependencies = [ + "mypy>=1.0.0", +] +[tool.hatch.envs.types.scripts] +check = "mypy --install-types --non-interactive {args:src/model tests}" + +[tool.coverage.run] +source_pkgs = ["model", "tests"] +branch = true +parallel = true +omit = [ + "src/model/__about__.py", +] + +[tool.coverage.paths] +model = ["src/model", "*/model/src/model"] +tests = ["tests", "*/model/tests"] + +[tool.coverage.report] +exclude_lines = [ + "no cov", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", +] diff --git a/src/View/__init__.py b/src/View/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/View/pyproject.toml b/src/View/pyproject.toml new file mode 100644 index 00000000..5bf5d769 --- /dev/null +++ b/src/View/pyproject.toml @@ -0,0 +1,74 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "view" +dynamic = ["version"] +description = 'The display for the user.' +readme = "README.md" +requires-python = ">=3.8" +license = "MIT" +keywords = [] + +classifiers = [ + "Development Status :: 4 - Beta", + "Programming Language :: Python", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] +dependencies = [] + +[tool.hatch.version] +path = "src/view/__about__.py" + +[tool.hatch.envs.default] +dependencies = [ + "coverage[toml]>=6.5", + "pytest", +] +[tool.hatch.envs.default.scripts] +test = "pytest {args:tests}" +test-cov = "coverage run -m pytest {args:tests}" +cov-report = [ + "- coverage combine", + "coverage report", +] +cov = [ + "test-cov", + "cov-report", +] + +[[tool.hatch.envs.all.matrix]] +python = ["3.8", "3.9", "3.10", "3.11", "3.12"] + +[tool.hatch.envs.types] +dependencies = [ + "mypy>=1.0.0", +] +[tool.hatch.envs.types.scripts] +check = "mypy --install-types --non-interactive {args:src/view tests}" + +[tool.coverage.run] +source_pkgs = ["view", "tests"] +branch = true +parallel = true +omit = [ + "src/view/__about__.py", +] + +[tool.coverage.paths] +view = ["src/view", "*/view/src/view"] +tests = ["tests", "*/view/tests"] + +[tool.coverage.report] +exclude_lines = [ + "no cov", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", +] From 1409da69d335937de6d401c57ec5bdca66931983 Mon Sep 17 00:00:00 2001 From: Peter Date: Fri, 16 Feb 2024 08:44:37 -0500 Subject: [PATCH 003/144] DO NOT MERGE --- .gitignore | 3 +- Controller.py | 255 ------------------------ CustomExceptions.py | 220 -------------------- Diagram.py | 223 --------------------- Entity.py | 95 --------- Help.py | 53 ----- Input.py | 32 --- Output.py | 25 --- Relation.py | 80 -------- Serializer.py | 83 -------- Test.py | 81 -------- pyvenv.cfg | 3 - testDiagram.py => test/testDiagram.py | 0 testHelp.py => test/testHelp.py | 0 testParseing.py => test/testParseing.py | 0 testTest.py => test/testTest.py | 0 16 files changed, 2 insertions(+), 1151 deletions(-) delete mode 100644 Controller.py delete mode 100644 CustomExceptions.py delete mode 100644 Diagram.py delete mode 100644 Entity.py delete mode 100644 Help.py delete mode 100644 Input.py delete mode 100644 Output.py delete mode 100644 Relation.py delete mode 100644 Serializer.py delete mode 100644 Test.py delete mode 100644 pyvenv.cfg rename testDiagram.py => test/testDiagram.py (100%) rename testHelp.py => test/testHelp.py (100%) rename testParseing.py => test/testParseing.py (100%) rename testTest.py => test/testTest.py (100%) diff --git a/.gitignore b/.gitignore index ebe2ab9f..7ae0cafe 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ __pycache__ Lib/ Scripts/ -Include/ \ No newline at end of file +Include/ +venv.cfg \ No newline at end of file diff --git a/Controller.py b/Controller.py deleted file mode 100644 index 74ca2e56..00000000 --- a/Controller.py +++ /dev/null @@ -1,255 +0,0 @@ -import Input -import Output -import Serializer -from CustomExceptions import CustomExceptions as CE -from Diagram import Diagram -import os -import Help - -#Parser Includes. These will be moved out when the parser is moved. -from Entity import Entity -from Relation import Relation - - - - -class Controller: - def __init__(self) -> None: - self._should_quit = False - self._diagram = Diagram() - - #map relating commands to the flags that can be passed to them - #NOTE: These must be synched with the command_function_map based on idx - self._command_flag_map = { - "class" : ["a","d","r"], - "list" : ["a","c","r","d"], - "att" : ["a","d","r"], - "rel" : ["a","d"], - "save" : [""], - "load" : [""], - "exit" : [""], - "quit" : [""], - "help" : [""] - } - - #map relating commands to the names of methods that can be called on them - #NOTE: these must be synched with command_flag_map based on idx - self._command_function_map = { - "class" : ["add_entity","delete_entity","rename_entity"], - "list" : ["list_everything","list_entities","list_relations","list_entity_details"], - "att" : ["add_attribute","delete_attribute","rename_attribute"], - "rel" : ["add_relation","delete_relation"], - "save" : ["save"], - "load" : ["load"], - "exit" : ["quit"], - "quit" : ["quit"], - "help" : ["help"] - } - - def run(self) -> None: - while not self._should_quit: - s = Input.read_line() - - try: - #parse the command - input = self.parse(s) - - #return from input is [function object, arg1,...,argn] - command = input[0] - args = input[1:] - - #execute the command - out = command(*args) - - #write output if it was something - if out != None: - Output.write(out) - - except TypeError as t: - Output.write(CE.InvalidArgCountError(t)) - except ValueError as v: - Output.write(CE.NeedsMoreInput()) - except Exception as e: - Output.write(str(e)) - - def quit(self): - '''Basic Quit Routine. Prompts user to save, where to save, - validates input. - - Returns: - If the name and filepath were valid or user doesn't want to save, returns true - If name is invalid, returns invalid filename exception - If filepath is invalid, returns invalid filepath exception - ''' - self._should_quit = True - while True: - answer = Input.read_line('Would you like to save before quit? [Y]/n: ').strip() - if not answer or answer in ['Y', 'n']: # default or Y/n - break - if answer == 'n': - #user wants to quit without saving - return - else: - answer = Input.read_line('Name of file to save: ') - - if isinstance(self.__check_args([answer]), Exception): - return CE.IOFailedError("Save", "invalid filename") - - self.save(answer) - - def save(self, name: str) -> None: - ''' - Saves the current diagram using a serializer with the given filename - - #### Parameters: - - `name` (str): The name of the file to be saved. - ''' - path = os.path.join(os.path.dirname(__file__), 'save') - if not os.path.exists(path): - os.makedirs(path) - path = os.path.join(path, name + '.json') - Serializer.serialize(diagram=self._diagram, path=path) - - def load(self, name: str) -> None: - ''' - Loads a diagram with the given filename using a deserializer. - - #### Parameters: - - `path` (str): The name of the file to be loaded. - ''' - path = os.path.join(os.path.dirname(__file__), 'save') - if not os.path.exists(path): - os.makedirs(path) - path = os.path.join(path, name + '.json') - loadedDiagram = Diagram() - Serializer.deserialize(diagram=loadedDiagram, path=path) - self._diagram = loadedDiagram - - def parse (self, input:str) -> list: - '''Parses a line of user input. - - Args: - input(str) - the line to be parsed - - Return: - With proper input: a list in the form [function, arg1,...,argN] - With invalid name: CustomExceptions.InvalidArgumentError - With invalid flag: CustomExceptions.InvalidFlagError - With invalid command: CustomExceptions.CommandNotFoundError - ''' - #guarding no input saves resources - if not input: - raise CE.NoInputError() - - #actual input to be parsed, split on spaces - bits = input.split() - - #Get the command that will be run - command_str = "" - #list slicing generates an empty list instead of an IndexError - command_str = self.__find_function(bits[0:1], bits[1:2]) - - #Get the args that will be passed to that command - args = [] - if not str(bits[1:2]).__contains__("-"): - args = self.__check_args(bits[1:]) - else: - args = self.__check_args(bits[2:]) - - #Get the class the command is in - command_class = self.__find_class(command_str) - - #go from knowing which class to having a specific instance - #of the class that the method needs to be called on - obj = self - if command_class == Diagram: - obj = self._diagram - elif command_class == Entity: - #if no args were provided, no entity can be found. Generate an error about invalid args - if not args: - raise CE.NoArgsGivenError() - - #if the method is in entity, get entity that needs to be changed - #pop the first element of args because it is the entity name, not a method param - obj = self._diagram.get_entity(args.pop(0)) - elif command_class == Help: - obj = Help - - #build and return the callable + args - return [getattr(obj, command_str)] + args - - - def __check_args(self, args:list): - '''Given a list of args, checks to make sure each one is valid. - Valid is defined as alphanumeric. - - Args: - args(list): a list of strings to be checked - - Return: - CustomExceptions.InvalidArgumentError if an argument provided is invalid - The list of args provided if all args are valid - ''' - for arg in args: - if not(arg.isalnum()): - raise CE.InvalidArgumentError(arg) - return args - - def __find_function(self, command:list, flag:list): - '''Finds the name of the function that should be called - - Args: - command - the command that was given to the parser - flag - the flag that was given to the parser - - Raises: - CustomExceptions.CommandNotFoundError if the command entered is - invalid - CustomExceptions.InvalidFlagError if the flag entered is invalid - - Returns: - The name of the function that needs to be called - ''' - #convert the params to strings - command = str(command[0]) - flag = str(flag[0]) if len(flag) > 0 else "" - - #check if the list of keys in the commmand flag map contains the given command - command_list = list(self._command_flag_map.keys()) - - valid_command = command_list.__contains__(command) - if not valid_command: - raise CE.CommandNotFoundError(command) - - #pull the list of flags for the validated command - flag_list = self._command_flag_map[command] - - #Make sure that the flag is a flag (preceded with -) - #some commands are just "command arg" so this needs to be checked - prepped_flag = "" - valid_flag = True - if flag.__contains__("-"): - prepped_flag = flag.lstrip("-") - valid_flag = flag_list.__contains__(prepped_flag) - - if not valid_flag: - raise CE.InvalidFlagError(flag, command) - - #compiling the correct location to index into the function map - flag_index = flag_list.index(prepped_flag) - flags = self._command_function_map.get(command) - return flags[flag_index] - - def __find_class(self, function:str): - '''Takes a function and locates the class that it exists in - - Args: - function - the function to locate in a class - - Returns: - the class the function originates in''' - classes = [Diagram, Entity, Relation, Help] - for cl in classes: - if hasattr(cl, function): - return cl - diff --git a/CustomExceptions.py b/CustomExceptions.py deleted file mode 100644 index 3c87d12a..00000000 --- a/CustomExceptions.py +++ /dev/null @@ -1,220 +0,0 @@ -import re #for argc errors -class CustomExceptions: - class Error(Exception): - """Base class for other exceptions.""" - pass - - #=============================================================================== - #Entity Exceptions - #=============================================================================== - class EntityExistsError(Error): - """Exception raised when an entity with a given name already exists. - - Args: - name (str): The name of the existing entity. - """ - def __init__(self, name) -> None: - super().__init__(f"Entity with name '{name}' already exists.") - - class EntityNotFoundError(Error): - """ - Exception raised when an entity with the given name is not found. - - Args: - name (str): The name of the entity not found. - """ - def __init__(self, name) -> None: - super().__init__(f"Entity with name '{name}' does not exist.") - - #=============================================================================== - #Attribute Exceptions - #=============================================================================== - class AttributeExistsError(Error): - """Exception raised when an attribute with a given name already exists. - - Args: - attr (str): The name of the existing attribute. - """ - def __init__(self, attr) -> None: - super().__init__(f"Attribute with name '{attr}' already exists.") - - class AttributeNotFoundError(Error): - """Exception raised when an attribute with a given name is not found. - - Args: - attr (str): The name of the attribute not found. - """ - def __init__(self, attr) -> None: - super().__init__(f"Attribute with name '{attr}' does not exist.") - - #=============================================================================== - #Relation Exceptions - #=============================================================================== - class RelationExistsError(Error): - """ - Exception raised when the relation being added already exists. - - Args: - source (Entity): The source of the relation that was being added. - destination (Entity): The destination of the relation that was - being added. - - """ - def __init__(self, source, destination): - super().__init__(f"Relation between '{source} -> {destination}' already exists.") - - class RelationDoesNotExistError(Error): - """ - Exception raised when the relation being deleted does not exist. - - Args: - source (Entity): The source of the relation that was being deleted. - destination (Entity): The destination of the relation that was - being deleted. - - """ - def __init__(self, source, destination): - super().__init__(f"Relation between '{source} -> {destination}' does not exist.") - - #=============================================================================== - #Parser/Controller Exceptions - #=============================================================================== - class InvalidArgumentError(Error): - """ - Exception raised when an input argument is not valid. - - Args: - name (str): The name of the argument that was not found. - """ - def __init__(self, name) -> None: - super().__init__(f"Argument '{name}' is not alphanumeric.") - - class InvalidFlagError(Error): - """ - Exception raised when an input flag is not valid. - - Args: - flag (str): The name of the flag that was not found. - command (str): The name of the command that was called with the invalid flag. - """ - def __init__(self, flag, command) -> None: - super().__init__(f"Command '{command}' has no flag '{flag}'.") - - class NoEntitySelected(Error): - """ - Exception raised when an input argument is not valid. - - Args: - flag (str): The name of the argument that was not found. - command (str): The name of the command that was called with the invalid flag. - """ - def __init__(self) -> None: - super().__init__(f"No class selected. Use 'class -s name' to select a class.") - - class CommandNotFoundError(Error): - """Exception raised when an invalid command is entered""" - - def __init__(self, name) -> None: - super().__init__(f"Command '{name}' does not exist. Try again or type 'help' for help.") - - class InvalidArgCountError(Error): - ''' - Exception raised when a user enters too many or too few arguments to the command they want to call. - - NOTE: This method really just catches the TypeError python generates and makes the message mroe readable. - - Args: - error - the TypeError that needs to be replaced - ''' - def __init__(self, error): - #matching the different syntaxes of TypeErrors - match = re.search(r".*(\d+).*(\d+).*", str(error)) - if match == None: - match = re.search(r".*(\d+).*", str(error)) - super().__init__(f"{match.group(1)} too few arguments given.") - else: - super().__init__(f"Expected {match.group(1)} arguments, but {match.group(2)} were given.") - - class NoArgsGivenError(Error): - ''' - Exception raised when a user gives no args to a command that requires one to be parsed - ''' - def __init__(self): - super().__init__(f"This command requires additional input. Type 'help' for command usage.") - - class NoInputError(Error): - ''' - Exception raised when no input is given and enter is hit - ''' - def __init__(self): - super().__init__(f"") - - class NeedsMoreInput(Error): - ''' - Exception raised when user gives only a command name - ''' - def __init__(self): - super().__init__(f"This command requires more input. Please try again or type 'help' for command usage.") - #=============================================================================== - #I/O Exceptions - #=============================================================================== - - class IOFailedError(Error): - '''Exception raised when an I/O Operation fails (saving or loading) - - Args: - opname (str): the name of the operation that failed - reason (str): the reason that opname failed - ''' - def __init__(self, opname, reason) -> None: - super().__init__(f"{opname} failed due to {reason}. Please try again.") - - class ReadFileError(Error): - '''Exception raised when failed to read a file - - #### Args: - `filepath` (str): the path of the file to read - ''' - def __init__(self, filepath: str) -> None: - super().__init__('Can not read file: "{}".'.format(filepath)) - - class WriteFileError(Error): - '''Exception raised when failed to write a file - - #### Args: - `filepath` (str): the path of the file to write - ''' - def __init__(self, filepath: str) -> None: - super().__init__('Can not write file: "{}".'.format(filepath)) - - #=============================================================================== - #Serializer Exceptions - #=============================================================================== - - class JsonDecodeError(Error): - '''Exception raised when failed to decode a Json file - - #### Args: - `filepath` (str): the path of the Json file to decode - ''' - def __init__(self, filepath: str) -> None: - super().__init__('Can not decode .json file: "{}".'.format(filepath)) - - class JsonEncodeError(Error): - '''Exception raised when failed to encode a Json file - - #### Args: - `filepath` (str): the path of the Json file to encode - ''' - def __init__(self, filepath: str) -> None: - super().__init__('Can not encode .json file: "{}".'.format(filepath)) - - class SavedDataError(Error): - '''Exception raised when the saved data is not consistent with the Diagram. - - #### Args: - `filepath` (str): the path of the save file - ''' - def __init__(self, filepath: str) -> None: - fmt = 'Failed to load save data: "{}".(Data in this save is no longer valid)' - super().__init__(fmt.format(filepath)) \ No newline at end of file diff --git a/Diagram.py b/Diagram.py deleted file mode 100644 index 9f913738..00000000 --- a/Diagram.py +++ /dev/null @@ -1,223 +0,0 @@ -from math import e -from Entity import Entity -from Relation import Relation -from CustomExceptions import CustomExceptions - -class Diagram: - def __init__(self) -> None: - self._entities = {} - self._relations = [] - - def add_entity(self, name: str): - """ - Adds an entity with the given name to the Diagram. - - Args: - name (str): The name of the entity to add. - - Raises: - CustomExceptions.EntityExistsError: If an entity - with the same name already exists. - - Returns: - None - """ - if name in self._entities: - raise CustomExceptions.EntityExistsError(name) - else: - self._entities[name] = Entity(name) - - def get_entity(self, name: str): - """ - Retrieves an entity from the diagram if it exists. - - Args: - name (str): The name of the entity to be retrieved. - - Raises: - CustomExceptions.EntityNotFoundError if the entity requested does not exist - - Returns: - Entity: If the entity exists. - None: If the entity does not exist. - """ - entity = self._entities.get(name, None) - if entity is None: - raise CustomExceptions.EntityNotFoundError(name) - else: - return entity - - - def delete_entity(self, name: str): - """ - Delete a given entity from the diagram. - - Args: - name (str): The name of the entity to be deleted. - - Raises: - CustomExceptions.EntityNotFoundError: If an entity to be deleted - does not exist. - - Returns: - None - """ - if name not in self._entities: - raise CustomExceptions.EntityNotFoundError(name) - - # Check for relations involving the entity and remove them - else: - relations_to_remove = [] - for rel in self._relations: - if rel.contains(self._entities[name]): - relations_to_remove.append(rel) - for rel in relations_to_remove: - self._relations.remove(rel) - - # Remove the entity from the dictionary - del self._entities[name] - - - def rename_entity(self, old_name: str, new_name: str): - """ - Rename a given entity with a new name. - - Args: - old_name (str): The current name of the entity to be renamed. - new_name (str): The new name for the entity. - - Raises: - CustomExceptions.EntityNotFoundError: If an entity with the old name - does not exist. - CustomExceptions.EntityExistsError: If an entity with the new name - already exists. - - Returns: - None - """ - if old_name not in self._entities: - raise CustomExceptions.EntityNotFoundError(old_name) - elif new_name in self._entities: - raise CustomExceptions.EntityExistsError(new_name) - # Update key - else: - entity = self._entities[old_name] - entity.set_name(new_name) - self._entities[new_name] = self._entities.pop(old_name) - - def list_everything(self): - """ - Returns a representation of the entire diagram. - - Returns: - str: A templated representation of all entities, their attributes, - and their relations. - """ - result = "" - for entity in self._entities.values(): - result += self.list_entity_details(entity.get_name()) + "\n" - return result - - def list_entity_details(self, entity_name): - """ - Returns the attributes and relations of the entity. - - Args: - entity_name (str): The name of the entity to get details of. - - Raises: - None - - Returns: - str: A templated string containing the attributes and relations. - """ - if not self._entities.__contains__(entity_name): - raise CustomExceptions.EntityNotFoundError(entity_name) - else: - entity = self._entities[entity_name] - att = entity._attributes - rels = [rel for rel in self._relations if rel.contains(entity)] - result ="\n" + entity_name + "'s Attributes:\n" - att_string = ', '.join(att) - result2 = entity_name + "'s Relations:\n" - rel_string = ', '.join(str(rel) for rel in rels) - return result + att_string + "\n" + result2 + rel_string - - def list_entities(self): - """ - Returns the entities in the relation. - - Returns: - str: String containing names of all existing entities. - """ - entity_names = list(self._entities.keys()) - return '\n' + ', '.join(entity_names) - - def list_relations(self): - """ - Lists all existing relations as a string. - - Returns: - str: A string representation of all existing relations. - """ - relations_list = [] - for rel in self._relations: - relations_list.append(str(rel)) - return '\n'.join(relations_list) - - - def add_relation(self, source, destination): - """ - Adds a relation between two Entities. - - Args: - source (str): The name of the source entity. - destination (str): The name of the destination entity. - - Raises: - CustomExceptions.EntityNotFoundError: If either the source or the - destination entity are not found. - CustomExceptions.RelationExistsError: If a relation already exists - between the source and destination entities. - """ - # Check for valid source and destination - if source not in self._entities: - raise CustomExceptions.EntityNotFoundError(source) - elif destination not in self._entities: - raise CustomExceptions.EntityNotFoundError(destination) - # Check for duplicate relationship containing same source and destination - else: - for rel in self._relations: - if rel.get_source() == self._entities[source] and rel.get_destination() == self._entities[destination]: - raise CustomExceptions.RelationExistsError(source, destination) - # Pass entity objects to relation and add relation to list of existing relations - relationship = Relation(self._entities[source], self._entities[destination]) - self._relations.append(relationship) - - def delete_relation(self, source, destination): - """ - Deletes a relation between two Entities. - - Args: - source (str): The name of the source entity. - destination (str): The name of the destination entity. - - Raises: - CustomExceptions.EntityNotFoundError: If either the source or the - destination entity is not found. - CustomExceptions.RelationDoesNotExistError: If a relation does not - exist between the source and destination entities. - """ - # Check for valid source and destination - if source not in self._entities: - raise CustomExceptions.EntityNotFoundError(source) - elif destination not in self._entities: - raise CustomExceptions.EntityNotFoundError(destination) - # Look for matching relation to delete - else: - for i, rel in enumerate(self._relations): - if rel.get_source() == self._entities[source] and rel.get_destination() == self._entities[destination]: - del self._relations[i] - return - raise CustomExceptions.RelationDoesNotExistError(source, destination) - diff --git a/Entity.py b/Entity.py deleted file mode 100644 index 3aba76ea..00000000 --- a/Entity.py +++ /dev/null @@ -1,95 +0,0 @@ -from CustomExceptions import CustomExceptions - -class Entity: - def __init__(self, name:str='') -> None: - """ - Constructs a new Entity object. - - Args: - name (str): The name of the entity. - """ - self.set_name(name) - self._attributes = set() - - def get_name(self) -> str: - ''' - Returns the name of the entity. - - # Returns: - (str): The name of the entity - ''' - return self._name - - def set_name(self, name: str) -> None: - """ - Update entity name. - - Args: - name (str): The new name for the entity. - """ - self._name = name - - def add_attribute(self, attr:str) -> None: - """ - Adds a new attribute to the to the entity. - - Args: - attr (str): The attribute name to be added to the entity. - - Raises: - CustomExceptions.AttributeExistsError: If the attribute already - exists in the Entity. - """ - if attr in self._attributes: - raise CustomExceptions.AttributeExistsError(attr) - else: - self._attributes.add(attr) - - def delete_attribute(self, attr: str) -> None: - """ - Deletes an attribute from this entity if it exists. - - Args: - attr (str): The name of the attribute to be deleted from the entity. - - Raises: - CustomExceptions.AttributeNotFoundError: If the specified attribute - is not found in the entity's attributes. - """ - if attr not in self._attributes: - raise CustomExceptions.AttributeNotFoundError(attr) - else: - self._attributes.remove(attr) - - def rename_attribute(self, old_attribute: str, new_attribute: str) -> None: - """ - Renames an attribute from its old name to a new name - - Args: - oldAttribute (str): The current name of the attribute. - newAttribute (str): The new name for the attribute - - Raises: - CustomExceptions.AttributeNotFoundError: If the old attribute does - not exist in the entity. - CustomExceptions.AttributeExistsError: If the new name is already - used for another attribute in this entity. - """ - if old_attribute not in self._attributes: - raise CustomExceptions.AttributeNotFoundError(old_attribute) - - elif new_attribute in self._attributes: - raise CustomExceptions.AttributeExistsError(new_attribute) - # Remove the old attribute and add the new attribute name - else: - self._attributes.remove(old_attribute) - self._attributes.add(new_attribute) - - def __str__(self) -> str: - """ - Returns the string representation of the Entity object (its name). - - Returns: - str: The name of the entity. - """ - return self._name diff --git a/Help.py b/Help.py deleted file mode 100644 index 4ef689d7..00000000 --- a/Help.py +++ /dev/null @@ -1,53 +0,0 @@ -'''Application help menu, to be called when user asks for help.''' -def help(): - """ - Returns a string that contains the menu. - - Args: - None. - - Raises: - None. - - Returns: - str(): The help list to be printed. - """ - menu = ( - #A general description - "\nHelp menu: For the best view, resize your window so that this message and the bar at the end are on one line. |\n\n" - "Below are the commands you can call and an explanation of what each does. Anything inside single quotes is decided\n" - "by you! Enter the command, replacing anything in the single quotes, and the quotes themselves, with the name you\n" - "want to use.\n\n" - "A valid name is made up of any combination of letters and numbers.\n\n" - #Class Commands - "Class Commands: \n\t" - "class -a 'name' - adds a class with name 'name'. Cannot add classes with duplicate or invalid names\n\t" - "class -d 'name' - deletes a class with name 'name'\n\t" - "class -r 'old' 'new' - renames class 'old' to 'new'. Cannot rename classes to duplicate or invalid names\n" - #Attribute Commands - "Attribute Commands: \n\t" - "att -a class 'name' - adds an attribute with name 'name' to class 'class'\n\t" - "att -d class 'name' - deletes an attribute with name 'name' from class 'class' if one exists\n\t" - "att -r class 'old' 'new' - renames an attribute from name 'old' to name 'new' in class 'class'\n" - #Relation Commands - "Relation Commands:\n\t" - "rel -a 'src' 'dest' - adds a relationship between class 'src' and class 'dest' assuming both are valid\n\t" - "rel -d 'src' 'dest' - deletes a relationship between class 'src' and class 'dest' if one exists\n" - #List Commands - "List Flags: \n\t" - "list -a - list all classes and their contents in the UML Diagram\n\t" - "list -c - list all classes in the UML Diagram\n\t" - "list -r - list all relationships in the UML Diagram\n\t" - "list -d 'name' - list all contents of class 'name'\n" - #Save Commands - "Save Flags: \n\t" - "save 'name' - saves the UML Diagram as a JSON file with name 'name'\n" - #Load Commands - "Load Flags: \n\t" - "load 'name' - loads the file with name 'name.json' if one exists.\n\n" - #Exit Commands - "Exit Commands: \n\t" - "exit - terminates the program.\n\t" - "quit - terminates the program.\n" - ) - return menu diff --git a/Input.py b/Input.py deleted file mode 100644 index 6cb1ce5b..00000000 --- a/Input.py +++ /dev/null @@ -1,32 +0,0 @@ -from CustomExceptions import CustomExceptions as CE - -def read_line(s='Command: ') -> str: - """ - Print a message to cmd and get a line of input - - ## Parameters: - - `s` (str): The message to print before asking for input - - ## Returns: - - (str): A line of input - """ - return input(s) - -def read_file(path: str) -> str: - """ - Reads the contents of a file and returns the content as a string. - - #### Parameters: - - `path` (str): The path to the file to be read. - - #### Returns: - - (str): The content of the file as a string. - - #### Raises: - - (CustomExceptions.ReadFileError): If failed to read the file - """ - try: - with open(path, 'r') as file: - return file.read() - except Exception: - raise CE.ReadFileError(filepath=path) diff --git a/Output.py b/Output.py deleted file mode 100644 index c6599752..00000000 --- a/Output.py +++ /dev/null @@ -1,25 +0,0 @@ -from CustomExceptions import CustomExceptions as CE - -def write(s: str) -> None: - print(s) - -def write_file(path: str, content: str) -> None: - ''' - Writes the specified content to a file at the given path. - - # Parameters: - - `path` (str): The path to the file to be written. - - `content` (str): The content to be written to the file. - - # Returns: - - None - - # Raises: - - `FileNotFoundError`: If the specified file is not found. - - `IOError`: If there is an issue writing to the file. - - Other exceptions: Any other exceptions that may occur during the file writing process. - ''' - try: - open(path, 'w').write(content) - except Exception: - raise CE.WriteFileError(path) \ No newline at end of file diff --git a/Relation.py b/Relation.py deleted file mode 100644 index 4cb638c5..00000000 --- a/Relation.py +++ /dev/null @@ -1,80 +0,0 @@ -from Entity import Entity - -class Relation: - def __init__(self, source=Entity(), destination=Entity()): - """ - Creates a relation between a source entity to a destination entity. - - Args: - source (Entity): The entity at the start of the relation. - destination (Entity): The entity at the end of the relation. - - Raises: - None. - - Returns: - None. - """ - self._source = source - self._destination = destination - - def get_source(self): - """ - Returns the source entity of the the relation. - - Args: - None. - - Raises: - None. - - Returns: - source (Entity): The source entity of the relation. - """ - return self._source - - def get_destination(self): - """ - Returns the destination entity of the relation. - - Args: - None. - - Raises: - None. - - Returns: - destination (Entity): The destination entity of the relation. - """ - return self._destination - - def contains(self, entity: Entity): - """ - Checks if a given entity is part of the relation. - - Args: - entity (Entity): The entity to be checked. - - Returns: - bool: Returns True if the entity is the source or the destination - of the relation. Returns False if the entity is not in the relation. - """ - if entity == self._source or entity == self._destination: - return True - else: - return False - - def __str__(self): - """ - Returns a string representation of a relation. - - Args: - None. - - Raises: - None. - - Returns: - str: A string representation of the relation. - """ - return f'{self._source} -> {self._destination}' diff --git a/Serializer.py b/Serializer.py deleted file mode 100644 index 69679723..00000000 --- a/Serializer.py +++ /dev/null @@ -1,83 +0,0 @@ -import json - -class CustomJSONEncoder(json.JSONEncoder): - ''' - a custom JSON encoder that returns a list when it encounters a set - ''' - def default(self, obj): - ''' - The `default` method is a custom implementation within a class that inherits from the `json.JSONEncoder` class in Python. - It enhances the JSON serialization process by providing special handling for sets. - ''' - if isinstance(obj, set): - return list(obj) - return json.JSONEncoder.default(self, obj) - -import Input -import Output -from Diagram import Diagram -from Entity import Entity -from Relation import Relation -from CustomExceptions import CustomExceptions as CE - -def serialize(diagram: Diagram, path: str) -> None: - ''' - Serialize a diagram's entities and relations to a JSON file. - - #### Parameters: - - `diagram` (Diagram): The diagram object containing entities and relations to be serialized. - - `path` (str): The file path where the JSON file will be saved. - ''' - entities = {name: vars(obj) for name, obj in diagram._entities.items()} - relations = [] - for x in diagram._relations: - properties = vars(x) - for property_name, property_val in properties.items(): - if isinstance(property_val, Entity): - properties[property_name] = property_val.get_name() - relations.append(properties) - try: - content = json.dumps(obj={'entities': entities, 'relations': relations}, cls=CustomJSONEncoder) - except Exception: - raise CE.JsonEncodeError(filepath=path) - Output.write_file(path=path, content=content) - -def deserialize(diagram: Diagram, path: str) -> None: - ''' - Deserialize a diagram from a JSON file and populate its entities and relations. - - #### Parameters: - - `diagram` (Diagram): The diagram object to populate with deserialized data. - - `path` (str): The file path of the JSON file to deserialize. - - #### Raises: - - (CustomExceptions.JsonDecodeError): If failed to decode the file - - (CustomExceptions.SavedDataError): If file data is not consistent with the Diagram - ''' - content = Input.read_file(path) - try: - diagram_attributes = json.loads(content) - except Exception: - raise CE.JsonDecodeError(filepath=path) - try: - for attr_name, attr_obj in diagram_attributes.items(): - if attr_name == 'entities': - for name, properties in attr_obj.items(): - entity = Entity() - for property_name, property_val in properties.items(): - if isinstance(getattr(entity, property_name), set): # Because custom encoder save set as list - property_val = set(property_val) - setattr(entity, property_name, property_val) - diagram._entities[name] = entity - elif attr_name == 'relations': - for properties in attr_obj: - relation = Relation() - for property_name, property_val in properties.items(): - if isinstance(getattr(relation, property_name), Entity): - property_val = diagram._entities[property_val] - if isinstance(getattr(relation, property_name), set): # Because custom encoder save set as list - property_val = set(property_val) - setattr(relation, property_name, property_val) - diagram._relations.append(relation) - except Exception: - raise CE.SavedDataError(filepath=path) \ No newline at end of file diff --git a/Test.py b/Test.py deleted file mode 100644 index 37191896..00000000 --- a/Test.py +++ /dev/null @@ -1,81 +0,0 @@ -class Test: - def __init__(self, name:str, func): - self.func = func - self.name = name - - - def exec(self, detail:str, expected, *args): - ''' - Executes self.func, passing in all args in the order provided - - Args: - detail - a little extra info about what case is being tested - expected - the expected output of this test. Can take any form with a defined __str__ - *args - a variadic list of all arguments that self.func needs to run - - Return: - A string in the form "self.name detail - passed" if the test was passed - A string in the form "self.name detail - expected {} actual {}" if the test was failed - - ''' - try: - output = self.func(*args) - except Exception as e: - return self.__createOutput(detail, str(expected), str(e)) - - - return self.__createOutput(detail, str(expected), str(output)) - - - def checkUpdate(self, detail:str, expected, searchLoc, *args): - ''' - Executes self.func, passing in all args in the order provided - - Args: - detail - a little extra info about what case is being tested - expected - the expected output of this test. Can take any form with a defined __str__ - searchLoc - the function to call to search for the expected output - *args - a variadic list of all arguments that self.func needs to run - - Return: - A string in the form "self.name detail - passed" if the test was passed - A string in the form "self.name detail - expected {} actual {}" if the test was failed - - ''' - try: - self.func(*args) - except Exception as e: - return self.__createOutput(detail, str(expected), str(e)) - - return self.__createOutput(detail, str(expected), str(searchLoc())) - - '''Private helper that takes an expected and actual output, then checks and formats them. - param detail - a short message about what case is being tested specifically - param expected - the expected outcome to compare to - param actual - the actual outcome of the test - returns a string in the form "self.name detail - passed" if the test was passed - returns a string in the form "self.name detail - expected {} actual {}" if the test was failed - ''' - def __createOutput(self, detail:str, expected:str, actual:str): - ''' - Private helper to create the output string returned from a call to any method that runs a test. - - Args: - detail - a little extra info about what case is being tested - expected - the expected output of this test, as a string - actual - the actual output of this test, as a string - - Return: - A string in the form "self.name detail - passed" if the test was passed - A string in the form "self.name detail - expected {} actual {}" if the test was failed - - ''' - output = self.name + ": " + detail + " - " - - if(expected == actual): - return output + "passed" - else: - return output + "\n\tExpected: " + expected + "\n\tActual: " + actual - - - \ No newline at end of file diff --git a/pyvenv.cfg b/pyvenv.cfg deleted file mode 100644 index cb9987e0..00000000 --- a/pyvenv.cfg +++ /dev/null @@ -1,3 +0,0 @@ -home = C:\Users\pnzr0\AppData\Local\Programs\Python\Python39 -include-system-site-packages = false -version = 3.9.0 diff --git a/testDiagram.py b/test/testDiagram.py similarity index 100% rename from testDiagram.py rename to test/testDiagram.py diff --git a/testHelp.py b/test/testHelp.py similarity index 100% rename from testHelp.py rename to test/testHelp.py diff --git a/testParseing.py b/test/testParseing.py similarity index 100% rename from testParseing.py rename to test/testParseing.py diff --git a/testTest.py b/test/testTest.py similarity index 100% rename from testTest.py rename to test/testTest.py From f331823fd7118c1003fdb8ae93c6f5dc11392c05 Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 17 Feb 2024 22:52:35 -0500 Subject: [PATCH 004/144] Setup Tools Build Structure, first implementation. --- .gitignore | 2 + main.py | 4 +- setup.py | 16 ++++ src/Controller/__init__.py | 5 -- src/Controller/pyproject.toml | 74 ------------------- src/Model/__init__.py | 5 -- src/Model/pyproject.toml | 74 ------------------- src/View/pyproject.toml | 74 ------------------- src/{View => umleditor}/__init__.py | 0 src/umleditor/mvc_controller/__init__.py | 5 ++ .../mvc_controller/controller.py} | 40 +++++----- .../mvc_controller/controller_input.py} | 2 +- .../mvc_controller/controller_output.py} | 2 +- .../mvc_controller/serializer.py} | 16 ++-- src/umleditor/mvc_model/__init__.py | 5 ++ .../mvc_model/custom_exceptions.py} | 0 .../mvc_model/diagram.py} | 6 +- .../mvc_model/entity.py} | 2 +- .../mvc_model/help_command.py} | 0 .../mvc_model/relation.py} | 2 +- .../Test.py => umleditor/mvc_model/test.py} | 5 +- src/umleditor/mvc_view/__init__.py | 0 test/test_imports.py | 19 +++++ 23 files changed, 87 insertions(+), 271 deletions(-) create mode 100644 setup.py delete mode 100644 src/Controller/__init__.py delete mode 100644 src/Controller/pyproject.toml delete mode 100644 src/Model/__init__.py delete mode 100644 src/Model/pyproject.toml delete mode 100644 src/View/pyproject.toml rename src/{View => umleditor}/__init__.py (100%) create mode 100644 src/umleditor/mvc_controller/__init__.py rename src/{Controller/Controller.py => umleditor/mvc_controller/controller.py} (88%) rename src/{Controller/Input.py => umleditor/mvc_controller/controller_input.py} (93%) rename src/{Controller/Output.py => umleditor/mvc_controller/controller_output.py} (87%) rename src/{Controller/Serializer.py => umleditor/mvc_controller/serializer.py} (86%) create mode 100644 src/umleditor/mvc_model/__init__.py rename src/{Model/CustomExceptions.py => umleditor/mvc_model/custom_exceptions.py} (100%) rename src/{Model/Diagram.py => umleditor/mvc_model/diagram.py} (98%) rename src/{Model/Entity.py => umleditor/mvc_model/entity.py} (98%) rename src/{Model/Help.py => umleditor/mvc_model/help_command.py} (100%) rename src/{Model/Relation.py => umleditor/mvc_model/relation.py} (98%) rename src/{Model/Test.py => umleditor/mvc_model/test.py} (99%) create mode 100644 src/umleditor/mvc_view/__init__.py create mode 100644 test/test_imports.py diff --git a/.gitignore b/.gitignore index 7ae0cafe..3a1e50fd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ __pycache__ *.test +*.egg-info +*.vscode Lib/ Scripts/ diff --git a/main.py b/main.py index f2a63dfa..20ca957a 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,6 @@ -from Controller import Controller +# from Controller import Controller +# from umleditor.mvc_controller.controller import Controller +from umleditor.mvc_controller import Controller def debug_main(): app = Controller() diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..29a2bbdc --- /dev/null +++ b/setup.py @@ -0,0 +1,16 @@ +from setuptools import find_packages, setup + +setup( + name="umlplaceholdername", + version="0.2", + description="UML Editor", + readme = "README.md", + python_requires = ">= 3.8", + license = "MIT", + author="Ganga Acharya, Marshall Feng, Peter Freedman, Adam Glick-Lynch, Tim Moser", + author_email='grachary@millersville.edu, mdfeng@millersville.edu, pwfreedm@millersville.edu, ahglickl@millersville.edu, timbmoser@gmail.com', + url="https://github.com/mucsci-students/2024sp-420-CWorld.git", + packages=find_packages(where='src'), + include_package_data=True, + package_dir={'': 'src'}, +) \ No newline at end of file diff --git a/src/Controller/__init__.py b/src/Controller/__init__.py deleted file mode 100644 index b90a7a4a..00000000 --- a/src/Controller/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .Controller import Controller -from .Input import Input -from .Output import Output -from .Serializer import serialize -from .Serializer import deserialize \ No newline at end of file diff --git a/src/Controller/pyproject.toml b/src/Controller/pyproject.toml deleted file mode 100644 index 64f41ade..00000000 --- a/src/Controller/pyproject.toml +++ /dev/null @@ -1,74 +0,0 @@ -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[project] -name = "controller" -dynamic = ["version"] -description = 'Controls the data flow between Model and View.' -readme = "README.md" -requires-python = ">=3.8" -license = "MIT" -keywords = [] - -classifiers = [ - "Development Status :: 4 - Beta", - "Programming Language :: Python", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", -] -dependencies = [] - -[tool.hatch.version] -path = "src/controller/__about__.py" - -[tool.hatch.envs.default] -dependencies = [ - "coverage[toml]>=6.5", - "pytest", -] -[tool.hatch.envs.default.scripts] -test = "pytest {args:tests}" -test-cov = "coverage run -m pytest {args:tests}" -cov-report = [ - "- coverage combine", - "coverage report", -] -cov = [ - "test-cov", - "cov-report", -] - -[[tool.hatch.envs.all.matrix]] -python = ["3.8", "3.9", "3.10", "3.11", "3.12"] - -[tool.hatch.envs.types] -dependencies = [ - "mypy>=1.0.0", -] -[tool.hatch.envs.types.scripts] -check = "mypy --install-types --non-interactive {args:src/controller tests}" - -[tool.coverage.run] -source_pkgs = ["controller", "tests"] -branch = true -parallel = true -omit = [ - "src/controller/__about__.py", -] - -[tool.coverage.paths] -controller = ["src/controller", "*/controller/src/controller"] -tests = ["tests", "*/controller/tests"] - -[tool.coverage.report] -exclude_lines = [ - "no cov", - "if __name__ == .__main__.:", - "if TYPE_CHECKING:", -] diff --git a/src/Model/__init__.py b/src/Model/__init__.py deleted file mode 100644 index 43c86f9f..00000000 --- a/src/Model/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .CustomExceptions import CustomExceptions -from .Diagram import Diagram -from .Entity import Entity -from .Help import help_menu -from .Relation import Relation \ No newline at end of file diff --git a/src/Model/pyproject.toml b/src/Model/pyproject.toml deleted file mode 100644 index e9f7f43f..00000000 --- a/src/Model/pyproject.toml +++ /dev/null @@ -1,74 +0,0 @@ -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[project] -name = "model" -dynamic = ["version"] -description = 'The format and condition of the data in the UML Diagram.' -readme = "README.md" -requires-python = ">=3.8" -license = "MIT" -keywords = [] - -classifiers = [ - "Development Status :: 4 - Beta", - "Programming Language :: Python", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", -] -dependencies = [] - -[tool.hatch.version] -path = "src/model/__about__.py" - -[tool.hatch.envs.default] -dependencies = [ - "coverage[toml]>=6.5", - "pytest", -] -[tool.hatch.envs.default.scripts] -test = "pytest {args:tests}" -test-cov = "coverage run -m pytest {args:tests}" -cov-report = [ - "- coverage combine", - "coverage report", -] -cov = [ - "test-cov", - "cov-report", -] - -[[tool.hatch.envs.all.matrix]] -python = ["3.8", "3.9", "3.10", "3.11", "3.12"] - -[tool.hatch.envs.types] -dependencies = [ - "mypy>=1.0.0", -] -[tool.hatch.envs.types.scripts] -check = "mypy --install-types --non-interactive {args:src/model tests}" - -[tool.coverage.run] -source_pkgs = ["model", "tests"] -branch = true -parallel = true -omit = [ - "src/model/__about__.py", -] - -[tool.coverage.paths] -model = ["src/model", "*/model/src/model"] -tests = ["tests", "*/model/tests"] - -[tool.coverage.report] -exclude_lines = [ - "no cov", - "if __name__ == .__main__.:", - "if TYPE_CHECKING:", -] diff --git a/src/View/pyproject.toml b/src/View/pyproject.toml deleted file mode 100644 index 5bf5d769..00000000 --- a/src/View/pyproject.toml +++ /dev/null @@ -1,74 +0,0 @@ -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[project] -name = "view" -dynamic = ["version"] -description = 'The display for the user.' -readme = "README.md" -requires-python = ">=3.8" -license = "MIT" -keywords = [] - -classifiers = [ - "Development Status :: 4 - Beta", - "Programming Language :: Python", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", -] -dependencies = [] - -[tool.hatch.version] -path = "src/view/__about__.py" - -[tool.hatch.envs.default] -dependencies = [ - "coverage[toml]>=6.5", - "pytest", -] -[tool.hatch.envs.default.scripts] -test = "pytest {args:tests}" -test-cov = "coverage run -m pytest {args:tests}" -cov-report = [ - "- coverage combine", - "coverage report", -] -cov = [ - "test-cov", - "cov-report", -] - -[[tool.hatch.envs.all.matrix]] -python = ["3.8", "3.9", "3.10", "3.11", "3.12"] - -[tool.hatch.envs.types] -dependencies = [ - "mypy>=1.0.0", -] -[tool.hatch.envs.types.scripts] -check = "mypy --install-types --non-interactive {args:src/view tests}" - -[tool.coverage.run] -source_pkgs = ["view", "tests"] -branch = true -parallel = true -omit = [ - "src/view/__about__.py", -] - -[tool.coverage.paths] -view = ["src/view", "*/view/src/view"] -tests = ["tests", "*/view/tests"] - -[tool.coverage.report] -exclude_lines = [ - "no cov", - "if __name__ == .__main__.:", - "if TYPE_CHECKING:", -] diff --git a/src/View/__init__.py b/src/umleditor/__init__.py similarity index 100% rename from src/View/__init__.py rename to src/umleditor/__init__.py diff --git a/src/umleditor/mvc_controller/__init__.py b/src/umleditor/mvc_controller/__init__.py new file mode 100644 index 00000000..738cfb38 --- /dev/null +++ b/src/umleditor/mvc_controller/__init__.py @@ -0,0 +1,5 @@ +from umleditor.mvc_controller.controller import Controller +# We probably won't need these, but leaving for quick include later. +# from controller_input import read_file, read_line +# from controller_output import write, write_file +# from serializer import serialize, deserialize \ No newline at end of file diff --git a/src/Controller/Controller.py b/src/umleditor/mvc_controller/controller.py similarity index 88% rename from src/Controller/Controller.py rename to src/umleditor/mvc_controller/controller.py index 22854da8..14d88c38 100644 --- a/src/Controller/Controller.py +++ b/src/umleditor/mvc_controller/controller.py @@ -1,14 +1,14 @@ -import Input -import Output -import Serializer -from CustomExceptions import CustomExceptions as CE -from Diagram import Diagram +from .controller_input import read_file, read_line +import umleditor.mvc_controller.controller_output as controller_output +from .serializer import CustomJSONEncoder, serialize, deserialize +from umleditor.mvc_model import CustomExceptions as CE +from umleditor.mvc_model.diagram import Diagram +from umleditor.mvc_model import help_menu import os -import Help #Parser Includes. These will be moved out when the parser is moved. -from Entity import Entity -from Relation import Relation +from umleditor.mvc_model.entity import Entity +from umleditor.mvc_model.relation import Relation @@ -48,7 +48,7 @@ def __init__(self) -> None: def run(self) -> None: while not self._should_quit: - s = Input.read_line() + s = read_line() try: #parse the command @@ -63,14 +63,14 @@ def run(self) -> None: #write output if it was something if out != None: - Output.write(out) + controller_output.write(out) except TypeError as t: - Output.write(CE.InvalidArgCountError(t)) + controller_output.write(CE.InvalidArgCountError(t)) except ValueError as v: - Output.write(CE.NeedsMoreInput()) + controller_output.write(CE.NeedsMoreInput()) except Exception as e: - Output.write(str(e)) + controller_output.write(str(e)) def quit(self): '''Basic Quit Routine. Prompts user to save, where to save, @@ -83,14 +83,14 @@ def quit(self): ''' self._should_quit = True while True: - answer = Input.read_line('Would you like to save before quit? [Y]/n: ').strip() + answer = read_line('Would you like to save before quit? [Y]/n: ').strip() if not answer or answer in ['Y', 'n']: # default or Y/n break if answer == 'n': #user wants to quit without saving return else: - answer = Input.read_line('Name of file to save: ') + answer = read_line('Name of file to save: ') if isinstance(self.__check_args([answer]), Exception): return CE.IOFailedError("Save", "invalid filename") @@ -108,7 +108,7 @@ def save(self, name: str) -> None: if not os.path.exists(path): os.makedirs(path) path = os.path.join(path, name + '.json') - Serializer.serialize(diagram=self._diagram, path=path) + serialize(diagram=self._diagram, path=path) def load(self, name: str) -> None: ''' @@ -122,7 +122,7 @@ def load(self, name: str) -> None: os.makedirs(path) path = os.path.join(path, name + '.json') loadedDiagram = Diagram() - Serializer.deserialize(diagram=loadedDiagram, path=path) + deserialize(diagram=loadedDiagram, path=path) self._diagram = loadedDiagram def parse (self, input:str) -> list: @@ -172,8 +172,8 @@ def parse (self, input:str) -> list: #if the method is in entity, get entity that needs to be changed #pop the first element of args because it is the entity name, not a method param obj = self._diagram.get_entity(args.pop(0)) - elif command_class == Help: - obj = Help + elif command_class == help_menu: + obj = help_menu #build and return the callable + args return [getattr(obj, command_str)] + args @@ -248,7 +248,7 @@ def __find_class(self, function:str): Returns: the class the function originates in''' - classes = [Diagram, Entity, Relation, Help] + classes = [Diagram, Entity, Relation, help_menu] for cl in classes: if hasattr(cl, function): return cl diff --git a/src/Controller/Input.py b/src/umleditor/mvc_controller/controller_input.py similarity index 93% rename from src/Controller/Input.py rename to src/umleditor/mvc_controller/controller_input.py index 6cb1ce5b..1bcedc11 100644 --- a/src/Controller/Input.py +++ b/src/umleditor/mvc_controller/controller_input.py @@ -1,4 +1,4 @@ -from CustomExceptions import CustomExceptions as CE +from umleditor.mvc_model import CustomExceptions as CE def read_line(s='Command: ') -> str: """ diff --git a/src/Controller/Output.py b/src/umleditor/mvc_controller/controller_output.py similarity index 87% rename from src/Controller/Output.py rename to src/umleditor/mvc_controller/controller_output.py index c6599752..c2bbe9d5 100644 --- a/src/Controller/Output.py +++ b/src/umleditor/mvc_controller/controller_output.py @@ -1,4 +1,4 @@ -from CustomExceptions import CustomExceptions as CE +from umleditor.mvc_model import CustomExceptions as CE def write(s: str) -> None: print(s) diff --git a/src/Controller/Serializer.py b/src/umleditor/mvc_controller/serializer.py similarity index 86% rename from src/Controller/Serializer.py rename to src/umleditor/mvc_controller/serializer.py index 69679723..069011d0 100644 --- a/src/Controller/Serializer.py +++ b/src/umleditor/mvc_controller/serializer.py @@ -13,12 +13,12 @@ def default(self, obj): return list(obj) return json.JSONEncoder.default(self, obj) -import Input -import Output -from Diagram import Diagram -from Entity import Entity -from Relation import Relation -from CustomExceptions import CustomExceptions as CE +from umleditor.mvc_controller.controller_input import read_file, read_line +import umleditor.mvc_controller.controller_output as controller_output +from umleditor.mvc_model.diagram import Diagram +from umleditor.mvc_model.entity import Entity +from umleditor.mvc_model.relation import Relation +from umleditor.mvc_model.custom_exceptions import CustomExceptions as CE def serialize(diagram: Diagram, path: str) -> None: ''' @@ -40,7 +40,7 @@ def serialize(diagram: Diagram, path: str) -> None: content = json.dumps(obj={'entities': entities, 'relations': relations}, cls=CustomJSONEncoder) except Exception: raise CE.JsonEncodeError(filepath=path) - Output.write_file(path=path, content=content) + controller_output.write_file(path=path, content=content) def deserialize(diagram: Diagram, path: str) -> None: ''' @@ -54,7 +54,7 @@ def deserialize(diagram: Diagram, path: str) -> None: - (CustomExceptions.JsonDecodeError): If failed to decode the file - (CustomExceptions.SavedDataError): If file data is not consistent with the Diagram ''' - content = Input.read_file(path) + content = read_file(path) try: diagram_attributes = json.loads(content) except Exception: diff --git a/src/umleditor/mvc_model/__init__.py b/src/umleditor/mvc_model/__init__.py new file mode 100644 index 00000000..2fe3a173 --- /dev/null +++ b/src/umleditor/mvc_model/__init__.py @@ -0,0 +1,5 @@ +from .custom_exceptions import CustomExceptions +from .diagram import Diagram +from .entity import Entity +from .help_command import help_menu +from .relation import Relation \ No newline at end of file diff --git a/src/Model/CustomExceptions.py b/src/umleditor/mvc_model/custom_exceptions.py similarity index 100% rename from src/Model/CustomExceptions.py rename to src/umleditor/mvc_model/custom_exceptions.py diff --git a/src/Model/Diagram.py b/src/umleditor/mvc_model/diagram.py similarity index 98% rename from src/Model/Diagram.py rename to src/umleditor/mvc_model/diagram.py index 9f913738..988dbcf4 100644 --- a/src/Model/Diagram.py +++ b/src/umleditor/mvc_model/diagram.py @@ -1,7 +1,7 @@ from math import e -from Entity import Entity -from Relation import Relation -from CustomExceptions import CustomExceptions +from .entity import Entity +from .relation import Relation +from .custom_exceptions import CustomExceptions class Diagram: def __init__(self) -> None: diff --git a/src/Model/Entity.py b/src/umleditor/mvc_model/entity.py similarity index 98% rename from src/Model/Entity.py rename to src/umleditor/mvc_model/entity.py index 3aba76ea..006d4786 100644 --- a/src/Model/Entity.py +++ b/src/umleditor/mvc_model/entity.py @@ -1,4 +1,4 @@ -from CustomExceptions import CustomExceptions +from .custom_exceptions import CustomExceptions class Entity: def __init__(self, name:str='') -> None: diff --git a/src/Model/Help.py b/src/umleditor/mvc_model/help_command.py similarity index 100% rename from src/Model/Help.py rename to src/umleditor/mvc_model/help_command.py diff --git a/src/Model/Relation.py b/src/umleditor/mvc_model/relation.py similarity index 98% rename from src/Model/Relation.py rename to src/umleditor/mvc_model/relation.py index 4cb638c5..d0a18a41 100644 --- a/src/Model/Relation.py +++ b/src/umleditor/mvc_model/relation.py @@ -1,4 +1,4 @@ -from Entity import Entity +from .entity import Entity class Relation: def __init__(self, source=Entity(), destination=Entity()): diff --git a/src/Model/Test.py b/src/umleditor/mvc_model/test.py similarity index 99% rename from src/Model/Test.py rename to src/umleditor/mvc_model/test.py index 37191896..1dd74bb5 100644 --- a/src/Model/Test.py +++ b/src/umleditor/mvc_model/test.py @@ -5,7 +5,7 @@ def __init__(self, name:str, func): def exec(self, detail:str, expected, *args): - ''' + """ Executes self.func, passing in all args in the order provided Args: @@ -16,8 +16,7 @@ def exec(self, detail:str, expected, *args): Return: A string in the form "self.name detail - passed" if the test was passed A string in the form "self.name detail - expected {} actual {}" if the test was failed - - ''' + """ try: output = self.func(*args) except Exception as e: diff --git a/src/umleditor/mvc_view/__init__.py b/src/umleditor/mvc_view/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/test_imports.py b/test/test_imports.py new file mode 100644 index 00000000..d3b2f811 --- /dev/null +++ b/test/test_imports.py @@ -0,0 +1,19 @@ +def test_import_controller(): + from umleditor.mvc_controller import Controller + + assert Controller + +def test_import_diagram(): + from umleditor.mvc_model import Diagram + + assert Diagram + +def test_import_entity(): + from umleditor.mvc_model import Entity + + assert Entity + +def test_import_relation(): + from umleditor.mvc_model import Relation + + assert Relation \ No newline at end of file From 2b575d7b6247cb8f89f772e449b45258f94a547f Mon Sep 17 00:00:00 2001 From: Peter F Date: Sun, 18 Feb 2024 12:05:44 -0500 Subject: [PATCH 005/144] Basic updates: - added build directories to gitignore - modified project name in setup.py --- .gitignore | 9 ++++++++- dist/2024sp_420_cworld-1.0.0.tar.gz | Bin 42506 -> 0 bytes setup.py | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) delete mode 100644 dist/2024sp_420_cworld-1.0.0.tar.gz diff --git a/.gitignore b/.gitignore index 3a1e50fd..6ba6dc77 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,16 @@ +#generic excludes __pycache__ *.test *.egg-info *.vscode +#Venv directories and files Lib/ Scripts/ Include/ -venv.cfg \ No newline at end of file +venv.cfg +pyvenv.cfg + +#Build directories and files +build/ +dist/ \ No newline at end of file diff --git a/dist/2024sp_420_cworld-1.0.0.tar.gz b/dist/2024sp_420_cworld-1.0.0.tar.gz deleted file mode 100644 index 98ec448bd0bea99bd585f647398b7a3eaff16309..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 42506 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 Date: Sun, 18 Feb 2024 12:36:30 -0500 Subject: [PATCH 006/144] new folder for generated text files --- test/{ => generated text files}/attribute_add_input.txt | 0 test/{ => generated text files}/attribute_add_output.txt | 0 test/{ => generated text files}/attribute_delete_input.txt | 0 test/{ => generated text files}/attribute_delete_output.txt | 0 test/{ => generated text files}/attribute_rename_input.txt | 0 test/{ => generated text files}/attribute_rename_output.txt | 0 test/{ => generated text files}/auto_test_list.txt | 0 test/{ => generated text files}/class_add_input.txt | 0 test/{ => generated text files}/class_add_output.txt | 0 test/{ => generated text files}/class_delete_input.txt | 0 test/{ => generated text files}/class_delete_output.txt | 0 test/{ => generated text files}/class_rename_input.txt | 0 test/{ => generated text files}/class_rename_output.txt | 0 test/{ => generated text files}/exit_input.txt | 0 test/{ => generated text files}/exit_output.txt | 0 test/{ => generated text files}/help_input.txt | 0 test/{ => generated text files}/help_output.txt | 0 test/{ => generated text files}/list_class_input.txt | 0 test/{ => generated text files}/list_class_output.txt | 0 test/{ => generated text files}/list_classes_input.txt | 0 test/{ => generated text files}/list_classes_output.txt | 0 test/{ => generated text files}/list_relationships_input.txt | 0 test/{ => generated text files}/list_relationships_output.txt | 0 test/{ => generated text files}/load_input.txt | 0 test/{ => generated text files}/load_output.txt | 0 test/{ => generated text files}/relationship_add_input.txt | 0 test/{ => generated text files}/relationship_add_output.txt | 0 test/{ => generated text files}/relationship_delete_input.txt | 0 test/{ => generated text files}/relationship_delete_output.txt | 0 test/{ => generated text files}/save_input.txt | 0 test/{ => generated text files}/save_output.txt | 0 31 files changed, 0 insertions(+), 0 deletions(-) rename test/{ => generated text files}/attribute_add_input.txt (100%) rename test/{ => generated text files}/attribute_add_output.txt (100%) rename test/{ => generated text files}/attribute_delete_input.txt (100%) rename test/{ => generated text files}/attribute_delete_output.txt (100%) rename test/{ => generated text files}/attribute_rename_input.txt (100%) rename test/{ => generated text files}/attribute_rename_output.txt (100%) rename test/{ => generated text files}/auto_test_list.txt (100%) rename test/{ => generated text files}/class_add_input.txt (100%) rename test/{ => generated text files}/class_add_output.txt (100%) rename test/{ => generated text files}/class_delete_input.txt (100%) rename test/{ => generated text files}/class_delete_output.txt (100%) rename test/{ => generated text files}/class_rename_input.txt (100%) rename test/{ => generated text files}/class_rename_output.txt (100%) rename test/{ => generated text files}/exit_input.txt (100%) rename test/{ => generated text files}/exit_output.txt (100%) rename test/{ => generated text files}/help_input.txt (100%) rename test/{ => generated text files}/help_output.txt (100%) rename test/{ => generated text files}/list_class_input.txt (100%) rename test/{ => generated text files}/list_class_output.txt (100%) rename test/{ => generated text files}/list_classes_input.txt (100%) rename test/{ => generated text files}/list_classes_output.txt (100%) rename test/{ => generated text files}/list_relationships_input.txt (100%) rename test/{ => generated text files}/list_relationships_output.txt (100%) rename test/{ => generated text files}/load_input.txt (100%) rename test/{ => generated text files}/load_output.txt (100%) rename test/{ => generated text files}/relationship_add_input.txt (100%) rename test/{ => generated text files}/relationship_add_output.txt (100%) rename test/{ => generated text files}/relationship_delete_input.txt (100%) rename test/{ => generated text files}/relationship_delete_output.txt (100%) rename test/{ => generated text files}/save_input.txt (100%) rename test/{ => generated text files}/save_output.txt (100%) diff --git a/test/attribute_add_input.txt b/test/generated text files/attribute_add_input.txt similarity index 100% rename from test/attribute_add_input.txt rename to test/generated text files/attribute_add_input.txt diff --git a/test/attribute_add_output.txt b/test/generated text files/attribute_add_output.txt similarity index 100% rename from test/attribute_add_output.txt rename to test/generated text files/attribute_add_output.txt diff --git a/test/attribute_delete_input.txt b/test/generated text files/attribute_delete_input.txt similarity index 100% rename from test/attribute_delete_input.txt rename to test/generated text files/attribute_delete_input.txt diff --git a/test/attribute_delete_output.txt b/test/generated text files/attribute_delete_output.txt similarity index 100% rename from test/attribute_delete_output.txt rename to test/generated text files/attribute_delete_output.txt diff --git a/test/attribute_rename_input.txt b/test/generated text files/attribute_rename_input.txt similarity index 100% rename from test/attribute_rename_input.txt rename to test/generated text files/attribute_rename_input.txt diff --git a/test/attribute_rename_output.txt b/test/generated text files/attribute_rename_output.txt similarity index 100% rename from test/attribute_rename_output.txt rename to test/generated text files/attribute_rename_output.txt diff --git a/test/auto_test_list.txt b/test/generated text files/auto_test_list.txt similarity index 100% rename from test/auto_test_list.txt rename to test/generated text files/auto_test_list.txt diff --git a/test/class_add_input.txt b/test/generated text files/class_add_input.txt similarity index 100% rename from test/class_add_input.txt rename to test/generated text files/class_add_input.txt diff --git a/test/class_add_output.txt b/test/generated text files/class_add_output.txt similarity index 100% rename from test/class_add_output.txt rename to test/generated text files/class_add_output.txt diff --git a/test/class_delete_input.txt b/test/generated text files/class_delete_input.txt similarity index 100% rename from test/class_delete_input.txt rename to test/generated text files/class_delete_input.txt diff --git a/test/class_delete_output.txt b/test/generated text files/class_delete_output.txt similarity index 100% rename from test/class_delete_output.txt rename to test/generated text files/class_delete_output.txt diff --git a/test/class_rename_input.txt b/test/generated text files/class_rename_input.txt similarity index 100% rename from test/class_rename_input.txt rename to test/generated text files/class_rename_input.txt diff --git a/test/class_rename_output.txt b/test/generated text files/class_rename_output.txt similarity index 100% rename from test/class_rename_output.txt rename to test/generated text files/class_rename_output.txt diff --git a/test/exit_input.txt b/test/generated text files/exit_input.txt similarity index 100% rename from test/exit_input.txt rename to test/generated text files/exit_input.txt diff --git a/test/exit_output.txt b/test/generated text files/exit_output.txt similarity index 100% rename from test/exit_output.txt rename to test/generated text files/exit_output.txt diff --git a/test/help_input.txt b/test/generated text files/help_input.txt similarity index 100% rename from test/help_input.txt rename to test/generated text files/help_input.txt diff --git a/test/help_output.txt b/test/generated text files/help_output.txt similarity index 100% rename from test/help_output.txt rename to test/generated text files/help_output.txt diff --git a/test/list_class_input.txt b/test/generated text files/list_class_input.txt similarity index 100% rename from test/list_class_input.txt rename to test/generated text files/list_class_input.txt diff --git a/test/list_class_output.txt b/test/generated text files/list_class_output.txt similarity index 100% rename from test/list_class_output.txt rename to test/generated text files/list_class_output.txt diff --git a/test/list_classes_input.txt b/test/generated text files/list_classes_input.txt similarity index 100% rename from test/list_classes_input.txt rename to test/generated text files/list_classes_input.txt diff --git a/test/list_classes_output.txt b/test/generated text files/list_classes_output.txt similarity index 100% rename from test/list_classes_output.txt rename to test/generated text files/list_classes_output.txt diff --git a/test/list_relationships_input.txt b/test/generated text files/list_relationships_input.txt similarity index 100% rename from test/list_relationships_input.txt rename to test/generated text files/list_relationships_input.txt diff --git a/test/list_relationships_output.txt b/test/generated text files/list_relationships_output.txt similarity index 100% rename from test/list_relationships_output.txt rename to test/generated text files/list_relationships_output.txt diff --git a/test/load_input.txt b/test/generated text files/load_input.txt similarity index 100% rename from test/load_input.txt rename to test/generated text files/load_input.txt diff --git a/test/load_output.txt b/test/generated text files/load_output.txt similarity index 100% rename from test/load_output.txt rename to test/generated text files/load_output.txt diff --git a/test/relationship_add_input.txt b/test/generated text files/relationship_add_input.txt similarity index 100% rename from test/relationship_add_input.txt rename to test/generated text files/relationship_add_input.txt diff --git a/test/relationship_add_output.txt b/test/generated text files/relationship_add_output.txt similarity index 100% rename from test/relationship_add_output.txt rename to test/generated text files/relationship_add_output.txt diff --git a/test/relationship_delete_input.txt b/test/generated text files/relationship_delete_input.txt similarity index 100% rename from test/relationship_delete_input.txt rename to test/generated text files/relationship_delete_input.txt diff --git a/test/relationship_delete_output.txt b/test/generated text files/relationship_delete_output.txt similarity index 100% rename from test/relationship_delete_output.txt rename to test/generated text files/relationship_delete_output.txt diff --git a/test/save_input.txt b/test/generated text files/save_input.txt similarity index 100% rename from test/save_input.txt rename to test/generated text files/save_input.txt diff --git a/test/save_output.txt b/test/generated text files/save_output.txt similarity index 100% rename from test/save_output.txt rename to test/generated text files/save_output.txt From 42f28be032cde30f2a09419cce76a2e115a46308 Mon Sep 17 00:00:00 2001 From: Peter F Date: Sun, 18 Feb 2024 12:46:39 -0500 Subject: [PATCH 007/144] moved tests into src --- .gitignore | 6 +++++- {test => src/test}/auto_test.py | 0 .../test}/generated text files/attribute_add_input.txt | 0 .../test}/generated text files/attribute_add_output.txt | 0 .../test}/generated text files/attribute_delete_input.txt | 0 .../test}/generated text files/attribute_delete_output.txt | 0 .../test}/generated text files/attribute_rename_input.txt | 0 .../test}/generated text files/attribute_rename_output.txt | 0 {test => src/test}/generated text files/auto_test_list.txt | 0 {test => src/test}/generated text files/class_add_input.txt | 0 .../test}/generated text files/class_add_output.txt | 0 .../test}/generated text files/class_delete_input.txt | 0 .../test}/generated text files/class_delete_output.txt | 0 .../test}/generated text files/class_rename_input.txt | 0 .../test}/generated text files/class_rename_output.txt | 0 {test => src/test}/generated text files/exit_input.txt | 0 {test => src/test}/generated text files/exit_output.txt | 0 {test => src/test}/generated text files/help_input.txt | 0 {test => src/test}/generated text files/help_output.txt | 0 .../test}/generated text files/list_class_input.txt | 0 .../test}/generated text files/list_class_output.txt | 0 .../test}/generated text files/list_classes_input.txt | 0 .../test}/generated text files/list_classes_output.txt | 0 .../test}/generated text files/list_relationships_input.txt | 0 .../generated text files/list_relationships_output.txt | 0 {test => src/test}/generated text files/load_input.txt | 0 {test => src/test}/generated text files/load_output.txt | 0 .../test}/generated text files/relationship_add_input.txt | 0 .../test}/generated text files/relationship_add_output.txt | 0 .../generated text files/relationship_delete_input.txt | 0 .../generated text files/relationship_delete_output.txt | 0 {test => src/test}/generated text files/save_input.txt | 0 {test => src/test}/generated text files/save_output.txt | 0 {test => src/test}/testDiagram.py | 0 {test => src/test}/testHelp.py | 0 {test => src/test}/testParseing.py | 0 {test => src/test}/testTest.py | 0 {test => src/test}/test_imports.py | 0 38 files changed, 5 insertions(+), 1 deletion(-) rename {test => src/test}/auto_test.py (100%) rename {test => src/test}/generated text files/attribute_add_input.txt (100%) rename {test => src/test}/generated text files/attribute_add_output.txt (100%) rename {test => src/test}/generated text files/attribute_delete_input.txt (100%) rename {test => src/test}/generated text files/attribute_delete_output.txt (100%) rename {test => src/test}/generated text files/attribute_rename_input.txt (100%) rename {test => src/test}/generated text files/attribute_rename_output.txt (100%) rename {test => src/test}/generated text files/auto_test_list.txt (100%) rename {test => src/test}/generated text files/class_add_input.txt (100%) rename {test => src/test}/generated text files/class_add_output.txt (100%) rename {test => src/test}/generated text files/class_delete_input.txt (100%) rename {test => src/test}/generated text files/class_delete_output.txt (100%) rename {test => src/test}/generated text files/class_rename_input.txt (100%) rename {test => src/test}/generated text files/class_rename_output.txt (100%) rename {test => src/test}/generated text files/exit_input.txt (100%) rename {test => src/test}/generated text files/exit_output.txt (100%) rename {test => src/test}/generated text files/help_input.txt (100%) rename {test => src/test}/generated text files/help_output.txt (100%) rename {test => src/test}/generated text files/list_class_input.txt (100%) rename {test => src/test}/generated text files/list_class_output.txt (100%) rename {test => src/test}/generated text files/list_classes_input.txt (100%) rename {test => src/test}/generated text files/list_classes_output.txt (100%) rename {test => src/test}/generated text files/list_relationships_input.txt (100%) rename {test => src/test}/generated text files/list_relationships_output.txt (100%) rename {test => src/test}/generated text files/load_input.txt (100%) rename {test => src/test}/generated text files/load_output.txt (100%) rename {test => src/test}/generated text files/relationship_add_input.txt (100%) rename {test => src/test}/generated text files/relationship_add_output.txt (100%) rename {test => src/test}/generated text files/relationship_delete_input.txt (100%) rename {test => src/test}/generated text files/relationship_delete_output.txt (100%) rename {test => src/test}/generated text files/save_input.txt (100%) rename {test => src/test}/generated text files/save_output.txt (100%) rename {test => src/test}/testDiagram.py (100%) rename {test => src/test}/testHelp.py (100%) rename {test => src/test}/testParseing.py (100%) rename {test => src/test}/testTest.py (100%) rename {test => src/test}/test_imports.py (100%) diff --git a/.gitignore b/.gitignore index 6ba6dc77..8591f538 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,8 @@ pyvenv.cfg #Build directories and files build/ -dist/ \ No newline at end of file +dist/ +main.spec + +#pytest +.pytest_cache \ No newline at end of file diff --git a/test/auto_test.py b/src/test/auto_test.py similarity index 100% rename from test/auto_test.py rename to src/test/auto_test.py diff --git a/test/generated text files/attribute_add_input.txt b/src/test/generated text files/attribute_add_input.txt similarity index 100% rename from test/generated text files/attribute_add_input.txt rename to src/test/generated text files/attribute_add_input.txt diff --git a/test/generated text files/attribute_add_output.txt b/src/test/generated text files/attribute_add_output.txt similarity index 100% rename from test/generated text files/attribute_add_output.txt rename to src/test/generated text files/attribute_add_output.txt diff --git a/test/generated text files/attribute_delete_input.txt b/src/test/generated text files/attribute_delete_input.txt similarity index 100% rename from test/generated text files/attribute_delete_input.txt rename to src/test/generated text files/attribute_delete_input.txt diff --git a/test/generated text files/attribute_delete_output.txt b/src/test/generated text files/attribute_delete_output.txt similarity index 100% rename from test/generated text files/attribute_delete_output.txt rename to src/test/generated text files/attribute_delete_output.txt diff --git a/test/generated text files/attribute_rename_input.txt b/src/test/generated text files/attribute_rename_input.txt similarity index 100% rename from test/generated text files/attribute_rename_input.txt rename to src/test/generated text files/attribute_rename_input.txt diff --git a/test/generated text files/attribute_rename_output.txt b/src/test/generated text files/attribute_rename_output.txt similarity index 100% rename from test/generated text files/attribute_rename_output.txt rename to src/test/generated text files/attribute_rename_output.txt diff --git a/test/generated text files/auto_test_list.txt b/src/test/generated text files/auto_test_list.txt similarity index 100% rename from test/generated text files/auto_test_list.txt rename to src/test/generated text files/auto_test_list.txt diff --git a/test/generated text files/class_add_input.txt b/src/test/generated text files/class_add_input.txt similarity index 100% rename from test/generated text files/class_add_input.txt rename to src/test/generated text files/class_add_input.txt diff --git a/test/generated text files/class_add_output.txt b/src/test/generated text files/class_add_output.txt similarity index 100% rename from test/generated text files/class_add_output.txt rename to src/test/generated text files/class_add_output.txt diff --git a/test/generated text files/class_delete_input.txt b/src/test/generated text files/class_delete_input.txt similarity index 100% rename from test/generated text files/class_delete_input.txt rename to src/test/generated text files/class_delete_input.txt diff --git a/test/generated text files/class_delete_output.txt b/src/test/generated text files/class_delete_output.txt similarity index 100% rename from test/generated text files/class_delete_output.txt rename to src/test/generated text files/class_delete_output.txt diff --git a/test/generated text files/class_rename_input.txt b/src/test/generated text files/class_rename_input.txt similarity index 100% rename from test/generated text files/class_rename_input.txt rename to src/test/generated text files/class_rename_input.txt diff --git a/test/generated text files/class_rename_output.txt b/src/test/generated text files/class_rename_output.txt similarity index 100% rename from test/generated text files/class_rename_output.txt rename to src/test/generated text files/class_rename_output.txt diff --git a/test/generated text files/exit_input.txt b/src/test/generated text files/exit_input.txt similarity index 100% rename from test/generated text files/exit_input.txt rename to src/test/generated text files/exit_input.txt diff --git a/test/generated text files/exit_output.txt b/src/test/generated text files/exit_output.txt similarity index 100% rename from test/generated text files/exit_output.txt rename to src/test/generated text files/exit_output.txt diff --git a/test/generated text files/help_input.txt b/src/test/generated text files/help_input.txt similarity index 100% rename from test/generated text files/help_input.txt rename to src/test/generated text files/help_input.txt diff --git a/test/generated text files/help_output.txt b/src/test/generated text files/help_output.txt similarity index 100% rename from test/generated text files/help_output.txt rename to src/test/generated text files/help_output.txt diff --git a/test/generated text files/list_class_input.txt b/src/test/generated text files/list_class_input.txt similarity index 100% rename from test/generated text files/list_class_input.txt rename to src/test/generated text files/list_class_input.txt diff --git a/test/generated text files/list_class_output.txt b/src/test/generated text files/list_class_output.txt similarity index 100% rename from test/generated text files/list_class_output.txt rename to src/test/generated text files/list_class_output.txt diff --git a/test/generated text files/list_classes_input.txt b/src/test/generated text files/list_classes_input.txt similarity index 100% rename from test/generated text files/list_classes_input.txt rename to src/test/generated text files/list_classes_input.txt diff --git a/test/generated text files/list_classes_output.txt b/src/test/generated text files/list_classes_output.txt similarity index 100% rename from test/generated text files/list_classes_output.txt rename to src/test/generated text files/list_classes_output.txt diff --git a/test/generated text files/list_relationships_input.txt b/src/test/generated text files/list_relationships_input.txt similarity index 100% rename from test/generated text files/list_relationships_input.txt rename to src/test/generated text files/list_relationships_input.txt diff --git a/test/generated text files/list_relationships_output.txt b/src/test/generated text files/list_relationships_output.txt similarity index 100% rename from test/generated text files/list_relationships_output.txt rename to src/test/generated text files/list_relationships_output.txt diff --git a/test/generated text files/load_input.txt b/src/test/generated text files/load_input.txt similarity index 100% rename from test/generated text files/load_input.txt rename to src/test/generated text files/load_input.txt diff --git a/test/generated text files/load_output.txt b/src/test/generated text files/load_output.txt similarity index 100% rename from test/generated text files/load_output.txt rename to src/test/generated text files/load_output.txt diff --git a/test/generated text files/relationship_add_input.txt b/src/test/generated text files/relationship_add_input.txt similarity index 100% rename from test/generated text files/relationship_add_input.txt rename to src/test/generated text files/relationship_add_input.txt diff --git a/test/generated text files/relationship_add_output.txt b/src/test/generated text files/relationship_add_output.txt similarity index 100% rename from test/generated text files/relationship_add_output.txt rename to src/test/generated text files/relationship_add_output.txt diff --git a/test/generated text files/relationship_delete_input.txt b/src/test/generated text files/relationship_delete_input.txt similarity index 100% rename from test/generated text files/relationship_delete_input.txt rename to src/test/generated text files/relationship_delete_input.txt diff --git a/test/generated text files/relationship_delete_output.txt b/src/test/generated text files/relationship_delete_output.txt similarity index 100% rename from test/generated text files/relationship_delete_output.txt rename to src/test/generated text files/relationship_delete_output.txt diff --git a/test/generated text files/save_input.txt b/src/test/generated text files/save_input.txt similarity index 100% rename from test/generated text files/save_input.txt rename to src/test/generated text files/save_input.txt diff --git a/test/generated text files/save_output.txt b/src/test/generated text files/save_output.txt similarity index 100% rename from test/generated text files/save_output.txt rename to src/test/generated text files/save_output.txt diff --git a/test/testDiagram.py b/src/test/testDiagram.py similarity index 100% rename from test/testDiagram.py rename to src/test/testDiagram.py diff --git a/test/testHelp.py b/src/test/testHelp.py similarity index 100% rename from test/testHelp.py rename to src/test/testHelp.py diff --git a/test/testParseing.py b/src/test/testParseing.py similarity index 100% rename from test/testParseing.py rename to src/test/testParseing.py diff --git a/test/testTest.py b/src/test/testTest.py similarity index 100% rename from test/testTest.py rename to src/test/testTest.py diff --git a/test/test_imports.py b/src/test/test_imports.py similarity index 100% rename from test/test_imports.py rename to src/test/test_imports.py From da9eecead5f609ee2daa1be3d559e0b4150e21ef Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 19 Feb 2024 19:58:20 -0500 Subject: [PATCH 008/144] Minor test updates. Most passing as is. --- src/test/testHelp.py | 15 ------------- src/test/{testDiagram.py => test_diagram.py} | 12 +++++----- .../{testParseing.py => test_parseing.py} | 22 +++++++++---------- src/test/{testTest.py => test_test.py} | 4 ++-- 4 files changed, 19 insertions(+), 34 deletions(-) delete mode 100644 src/test/testHelp.py rename src/test/{testDiagram.py => test_diagram.py} (91%) rename src/test/{testParseing.py => test_parseing.py} (85%) rename src/test/{testTest.py => test_test.py} (91%) diff --git a/src/test/testHelp.py b/src/test/testHelp.py deleted file mode 100644 index 7ee29ab3..00000000 --- a/src/test/testHelp.py +++ /dev/null @@ -1,15 +0,0 @@ -import Help - -def main(): - #manually verifying these because automating them seems pointless. They are just strings. - - print(Help.basicHelp()) - print(Help.cmdHelp("class")) - print(Help.cmdHelp("att")) - print(Help.cmdHelp("rel")) - print(Help.cmdHelp("list")) - print(Help.cmdHelp("save")) - print(Help.cmdHelp("load")) - print(Help.cmdHelp("invalid command")) - -main() \ No newline at end of file diff --git a/src/test/testDiagram.py b/src/test/test_diagram.py similarity index 91% rename from src/test/testDiagram.py rename to src/test/test_diagram.py index 4d7c55e3..18d74731 100644 --- a/src/test/testDiagram.py +++ b/src/test/test_diagram.py @@ -1,9 +1,9 @@ import os -from Test import Test -from Diagram import Diagram -import Serializer -from Entity import Entity +from umleditor.mvc_model.test import Test +from umleditor.mvc_model.diagram import Diagram +from umleditor.mvc_controller.serializer import CustomJSONEncoder, serialize, deserialize +from umleditor.mvc_model.entity import Entity def main(): """ @@ -79,9 +79,9 @@ def main(): toSave.add_relation('First', 'Third') toSave.add_relation('Second', 'Third') dirname = os.path.dirname(__file__) - Serializer.serialize(diagram=toSave, path=os.path.join(dirname, 'save.test')) + serialize(diagram=toSave, path=os.path.join(dirname, 'save.test')) toLoad = Diagram() - Serializer.deserialize(diagram=toLoad, path=os.path.join(dirname, 'save.test')) + deserialize(diagram=toLoad, path=os.path.join(dirname, 'save.test')) res = 'Save/Load - {}' passed = toSave.list_entities() == toLoad.list_entities() and toSave.list_relations() == toLoad.list_relations() print(res.format('Passed' if passed else 'Failed')) diff --git a/src/test/testParseing.py b/src/test/test_parseing.py similarity index 85% rename from src/test/testParseing.py rename to src/test/test_parseing.py index 8df530a8..b54e6318 100644 --- a/src/test/testParseing.py +++ b/src/test/test_parseing.py @@ -1,16 +1,16 @@ -import Input -import Output -import Serializer -from CustomExceptions import CustomExceptions as CE -from Controller import Controller -from Diagram import Diagram -from Test import Test +from umleditor.mvc_controller.controller_input import read_file, read_line +from umleditor.mvc_controller.controller_output import write, write_file +from umleditor.mvc_controller.serializer import CustomJSONEncoder, serialize, deserialize +from umleditor.mvc_model.custom_exceptions import CustomExceptions as CE +from umleditor.mvc_controller.controller import Controller +from umleditor.mvc_model.diagram import Diagram +from umleditor.mvc_model.test import Test import os -import Help +# import Help #Parser Includes. These will be moved out when the parser is moved. -from Entity import Entity -from Relation import Relation +from umleditor.mvc_model.entity import Entity +from umleditor.mvc_model.relation import Relation def main(): c = Controller() @@ -67,7 +67,7 @@ def unit_tests(c:Controller): print(parseTest.exec("save", [c.save], "save")) print(parseTest.exec("load", [c.load], "load")) print(parseTest.exec("quit", [c.quit], "quit")) - print(parseTest.exec("help", [Help.help], "help")) + # print(parseTest.exec("help", [Help.help], "help")) print(parseTest.exec("command invalid", CE.CommandNotFoundError("z"), "z")) diff --git a/src/test/testTest.py b/src/test/test_test.py similarity index 91% rename from src/test/testTest.py rename to src/test/test_test.py index b32fb641..7ea37dd6 100644 --- a/src/test/testTest.py +++ b/src/test/test_test.py @@ -1,5 +1,5 @@ #Tests the class Test.py -from Test import Test +from umleditor.mvc_model.test import Test class Dummy: def __init__(self, val): @@ -27,7 +27,7 @@ def main(): subtracting = Test("Test subtract", subtractOne) print(subtracting.exec("5 - 3", 2, 5, 3)) - print(subtracting.exec("3 - 7", -4, 2, 7)) + print(subtracting.exec("3 - 7", -4, 3, 7)) stringmod = Test("fancyString", fancyString) print(stringmod.exec("idk", "idk according to all known laws of aviation", "idk")) From 225d6a812a60a1cf8401c9f83e00d626fea1df4c Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 20 Feb 2024 21:50:01 -0500 Subject: [PATCH 009/144] Progress towards Methods and Fields. --- src/umleditor/mvc_controller/controller.py | 6 +- src/umleditor/mvc_model/__init__.py | 2 +- src/umleditor/mvc_model/custom_exceptions.py | 44 ++++++-- src/umleditor/mvc_model/diagram.py | 2 +- src/umleditor/mvc_model/entity.py | 111 ++++++++++++------- 5 files changed, 113 insertions(+), 52 deletions(-) diff --git a/src/umleditor/mvc_controller/controller.py b/src/umleditor/mvc_controller/controller.py index 14d88c38..68eda54f 100644 --- a/src/umleditor/mvc_controller/controller.py +++ b/src/umleditor/mvc_controller/controller.py @@ -23,7 +23,8 @@ def __init__(self) -> None: self._command_flag_map = { "class" : ["a","d","r"], "list" : ["a","c","r","d"], - "att" : ["a","d","r"], + "fld" : ["a","d","r"], + "mthd" : ["a","d","r"], "rel" : ["a","d"], "save" : [""], "load" : [""], @@ -37,7 +38,8 @@ def __init__(self) -> None: self._command_function_map = { "class" : ["add_entity","delete_entity","rename_entity"], "list" : ["list_everything","list_entities","list_relations","list_entity_details"], - "att" : ["add_attribute","delete_attribute","rename_attribute"], + "fld" : ["add_field","delete_field","rename_field"], + "mthd" : ["add_method","delete_method","rename_method"], "rel" : ["add_relation","delete_relation"], "save" : ["save"], "load" : ["load"], diff --git a/src/umleditor/mvc_model/__init__.py b/src/umleditor/mvc_model/__init__.py index 2fe3a173..e791da4d 100644 --- a/src/umleditor/mvc_model/__init__.py +++ b/src/umleditor/mvc_model/__init__.py @@ -1,5 +1,5 @@ from .custom_exceptions import CustomExceptions from .diagram import Diagram -from .entity import Entity +from .entity import Entity, UML_Method from .help_command import help_menu from .relation import Relation \ No newline at end of file diff --git a/src/umleditor/mvc_model/custom_exceptions.py b/src/umleditor/mvc_model/custom_exceptions.py index 120fc1d0..9c4474f0 100644 --- a/src/umleditor/mvc_model/custom_exceptions.py +++ b/src/umleditor/mvc_model/custom_exceptions.py @@ -27,25 +27,25 @@ def __init__(self, name) -> None: super().__init__(f"Entity with name '{name}' does not exist.") #===============================================================================# - #Attribute Exceptions + #Field Exceptions #===============================================================================# - class AttributeExistsError(Error): - """Exception raised when an attribute with a given name already exists. + class FieldExistsError(Error): + """Exception raised when a field with a given name already exists. Args: - attr (str): The name of the existing attribute. + field_name (str): The name of the existing field. """ - def __init__(self, attr) -> None: - super().__init__(f"Attribute with name '{attr}' already exists.") + def __init__(self, field_name): + super().__init__(f"Field with name '{field_name}' already exists.") - class AttributeNotFoundError(Error): - """Exception raised when an attribute with a given name is not found. + class FieldNotFoundError(Error): + """Exception raised when a field with a given name is not found. Args: - attr (str): The name of the attribute not found. + field_name (str): The name of the field not found. """ - def __init__(self, attr) -> None: - super().__init__(f"Attribute with name '{attr}' does not exist.") + def __init__(self, field_name): + super().__init__(f"Field with name '{field_name}' does not exist.") #===============================================================================# #Relation Exceptions @@ -76,6 +76,28 @@ class RelationDoesNotExistError(Error): def __init__(self, source, destination): super().__init__(f"Relation between '{source} -> {destination}' does not exist.") + #===============================================================================# + #Method Exceptions + #===============================================================================# + class MethodExistsError(Error): + """ + Exception raised when the method already exists. + + Args: + method_name (str): The name of the method that exists. + """ + def __init__(self, method_name): + super().__init__(f"Method named: '{method_name}' already exists.") + + class MethodNotFoundError(Error): + """ + Exception raised when the method was not found. + + Args: + method_name (str): The name of the method that does not exist. + """ + def __init__(self, method_name): + super().__init__(f"Method named: '{method_name}' does not exist.") #===============================================================================# #Parser/Controller Exceptions #===============================================================================# diff --git a/src/umleditor/mvc_model/diagram.py b/src/umleditor/mvc_model/diagram.py index 988dbcf4..7ec3845a 100644 --- a/src/umleditor/mvc_model/diagram.py +++ b/src/umleditor/mvc_model/diagram.py @@ -1,5 +1,5 @@ from math import e -from .entity import Entity +from .entity import Entity, UML_Method from .relation import Relation from .custom_exceptions import CustomExceptions diff --git a/src/umleditor/mvc_model/entity.py b/src/umleditor/mvc_model/entity.py index 006d4786..745b2e53 100644 --- a/src/umleditor/mvc_model/entity.py +++ b/src/umleditor/mvc_model/entity.py @@ -1,17 +1,18 @@ from .custom_exceptions import CustomExceptions class Entity: - def __init__(self, name:str='') -> None: + def __init__(self, entity_name:str=''): """ Constructs a new Entity object. Args: - name (str): The name of the entity. + entity_name (str): The name of the entity. """ - self.set_name(name) - self._attributes = set() + self.set_name(entity_name) + self._fields = [str] + self._methods = [UML_Method] - def get_name(self) -> str: + def get_name(self): ''' Returns the name of the entity. @@ -20,70 +21,95 @@ def get_name(self) -> str: ''' return self._name - def set_name(self, name: str) -> None: + def set_name(self, entity_name: str): """ Update entity name. Args: - name (str): The new name for the entity. + entity_name (str): The new name for the entity. """ - self._name = name + self._name = entity_name - def add_attribute(self, attr:str) -> None: + def add_field(self, field_name:str): """ - Adds a new attribute to the to the entity. + Adds a new field to the to the list. Args: - attr (str): The attribute name to be added to the entity. + field_name (str): The field' name to be added to the entity. Raises: - CustomExceptions.AttributeExistsError: If the attribute already + CustomExceptions.FieldExistsError: If the field already exists in the Entity. """ - if attr in self._attributes: - raise CustomExceptions.AttributeExistsError(attr) + if field_name in self._fields: + raise CustomExceptions.FieldExistsError(field_name) else: - self._attributes.add(attr) + self._fields.append(field_name) - def delete_attribute(self, attr: str) -> None: + def delete_field(self, field_name: str): """ - Deletes an attribute from this entity if it exists. + Deletes a field from this entity if the field exists. Args: - attr (str): The name of the attribute to be deleted from the entity. + field_name (str): The name of the field to be deleted from the entity. Raises: - CustomExceptions.AttributeNotFoundError: If the specified attribute - is not found in the entity's attributes. + CustomExceptions.FieldNotFoundError: If the specified field + is not found in the entity's field list. """ - if attr not in self._attributes: - raise CustomExceptions.AttributeNotFoundError(attr) + if field_name not in self._fields: + raise CustomExceptions.FieldNotFoundError(field_name) else: - self._attributes.remove(attr) + self._fields.remove(field_name) - def rename_attribute(self, old_attribute: str, new_attribute: str) -> None: + def rename_field(self, old_field: str, new_field: str): """ - Renames an attribute from its old name to a new name + Renames a field from its old name to a new name Args: - oldAttribute (str): The current name of the attribute. - newAttribute (str): The new name for the attribute + old_field(str): The current name of the field. + new_field (str): The new name for the field. Raises: - CustomExceptions.AttributeNotFoundError: If the old attribute does + CustomExceptions.FieldNotFoundError: If the old field does not exist in the entity. - CustomExceptions.AttributeExistsError: If the new name is already - used for another attribute in this entity. + CustomExceptions.FieldExistsError: If the new name is already + used for another field in this entity. """ - if old_attribute not in self._attributes: - raise CustomExceptions.AttributeNotFoundError(old_attribute) + if old_field not in self._fields: + raise CustomExceptions.FieldNotFoundError(old_field) - elif new_attribute in self._attributes: - raise CustomExceptions.AttributeExistsError(new_attribute) - # Remove the old attribute and add the new attribute name + elif new_field in self._fields: + raise CustomExceptions.FieldExistsError(new_field) else: - self._attributes.remove(old_attribute) - self._attributes.add(new_attribute) + self._fields.remove(old_field) + self._fields.append(new_field) + + def add_method(self, method_name: str): + if any(method_name == um.get_name() for um in self._methods): + raise CustomExceptions.MethodExistsError(method_name) + else: + new_method = UML_Method(method_name) + self._methods.append(new_method) + + def delete_method(self, method_name: str): + for um in self._methods: + if um.get_name == method_name: + self._methods.remove(um) + else: + raise CustomExceptions.MethodNotFoundError(method_name) + + def rename_method(self, old_name: str, new_name: str): + if not any(old_name == um.get_name() for um in self._methods): + raise CustomExceptions.MethodNotFoundError(old_name) + elif any(new_name == um.get_name() for um in self._methods): + raise CustomExceptions.MethodExistsError(new_name) + else: + for um in self._methods: + if (old_name == um.get_name()): + self._methods.remove(um) + new_method = UML_Method(new_name) + self._methods.append(new_method) def __str__(self) -> str: """ @@ -93,3 +119,14 @@ def __str__(self) -> str: str: The name of the entity. """ return self._name + +class UML_Method: + def __init__(self, method_name): + self._name = method_name + self._params = [] + + def get_name(self): + return self._name + + def set_name(self, new_name): + self._name = new_name \ No newline at end of file From f609884f09525d28eb85158e224a98cb839c648f Mon Sep 17 00:00:00 2001 From: Peter Date: Wed, 21 Feb 2024 16:17:05 -0500 Subject: [PATCH 010/144] DO NOT COMMIT - Moved parser and lexer into their own files - updated gitignore for arch venv directories --- .gitignore | 6 + lib64 | 1 + src/umleditor/mvc_controller/controller.py | 163 +-------------------- src/umleditor/mvc_controller/uml_parser.py | 89 +++++++++++ src/umleditor/mvc_view/__init__.py | 1 + src/umleditor/mvc_view/cli_lexer.py | 74 ++++++++++ 6 files changed, 176 insertions(+), 158 deletions(-) create mode 120000 lib64 create mode 100644 src/umleditor/mvc_controller/uml_parser.py create mode 100644 src/umleditor/mvc_view/cli_lexer.py diff --git a/.gitignore b/.gitignore index 8591f538..a6b6ddc1 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,12 @@ Include/ venv.cfg pyvenv.cfg +#Linux venv directories +bin/ +include/ +lib/ +lib64/ + #Build directories and files build/ dist/ diff --git a/lib64 b/lib64 new file mode 120000 index 00000000..7951405f --- /dev/null +++ b/lib64 @@ -0,0 +1 @@ +lib \ No newline at end of file diff --git a/src/umleditor/mvc_controller/controller.py b/src/umleditor/mvc_controller/controller.py index 14d88c38..dba2f501 100644 --- a/src/umleditor/mvc_controller/controller.py +++ b/src/umleditor/mvc_controller/controller.py @@ -1,9 +1,11 @@ from .controller_input import read_file, read_line import umleditor.mvc_controller.controller_output as controller_output +from umleditor.mvc_controller.uml_parser import Parser from .serializer import CustomJSONEncoder, serialize, deserialize from umleditor.mvc_model import CustomExceptions as CE from umleditor.mvc_model.diagram import Diagram from umleditor.mvc_model import help_menu +from umleditor.mvc_controller.uml_parser import check_args import os #Parser Includes. These will be moved out when the parser is moved. @@ -18,41 +20,14 @@ def __init__(self) -> None: self._should_quit = False self._diagram = Diagram() - #map relating commands to the flags that can be passed to them - #NOTE: These must be synched with the command_function_map based on idx - self._command_flag_map = { - "class" : ["a","d","r"], - "list" : ["a","c","r","d"], - "att" : ["a","d","r"], - "rel" : ["a","d"], - "save" : [""], - "load" : [""], - "exit" : [""], - "quit" : [""], - "help" : [""] - } - - #map relating commands to the names of methods that can be called on them - #NOTE: these must be synched with command_flag_map based on idx - self._command_function_map = { - "class" : ["add_entity","delete_entity","rename_entity"], - "list" : ["list_everything","list_entities","list_relations","list_entity_details"], - "att" : ["add_attribute","delete_attribute","rename_attribute"], - "rel" : ["add_relation","delete_relation"], - "save" : ["save"], - "load" : ["load"], - "exit" : ["quit"], - "quit" : ["quit"], - "help" : ["help_menu"] - } def run(self) -> None: while not self._should_quit: s = read_line() - + p = Parser(self._diagram) try: #parse the command - input = self.parse(s) + input = p.parse(s) #return from input is [function object, arg1,...,argn] command = input[0] @@ -92,7 +67,7 @@ def quit(self): else: answer = read_line('Name of file to save: ') - if isinstance(self.__check_args([answer]), Exception): + if isinstance(check_args([answer]), Exception): return CE.IOFailedError("Save", "invalid filename") self.save(answer) @@ -125,131 +100,3 @@ def load(self, name: str) -> None: deserialize(diagram=loadedDiagram, path=path) self._diagram = loadedDiagram - def parse (self, input:str) -> list: - '''Parses a line of user input. - - Args: - input(str) - the line to be parsed - - Return: - With proper input: a list in the form [function, arg1,...,argN] - With invalid name: CustomExceptions.InvalidArgumentError - With invalid flag: CustomExceptions.InvalidFlagError - With invalid command: CustomExceptions.CommandNotFoundError - ''' - #guarding no input saves resources - if not input: - raise CE.NoInputError() - - #actual input to be parsed, split on spaces - bits = input.split() - - #Get the command that will be run - command_str = "" - #list slicing generates an empty list instead of an IndexError - command_str = self.__find_function(bits[0:1], bits[1:2]) - - #Get the args that will be passed to that command - args = [] - if not str(bits[1:2]).__contains__("-"): - args = self.__check_args(bits[1:]) - else: - args = self.__check_args(bits[2:]) - - #Get the class the command is in - command_class = self.__find_class(command_str) - - #go from knowing which class to having a specific instance - #of the class that the method needs to be called on - obj = self - if command_class == Diagram: - obj = self._diagram - elif command_class == Entity: - #if no args were provided, no entity can be found. Generate an error about invalid args - if not args: - raise CE.NoArgsGivenError() - - #if the method is in entity, get entity that needs to be changed - #pop the first element of args because it is the entity name, not a method param - obj = self._diagram.get_entity(args.pop(0)) - elif command_class == help_menu: - obj = help_menu - - #build and return the callable + args - return [getattr(obj, command_str)] + args - - - def __check_args(self, args:list): - '''Given a list of args, checks to make sure each one is valid. - Valid is defined as alphanumeric. - - Args: - args(list): a list of strings to be checked - - Return: - CustomExceptions.InvalidArgumentError if an argument provided is invalid - The list of args provided if all args are valid - ''' - for arg in args: - if not(arg.isalnum()): - raise CE.InvalidArgumentError(arg) - return args - - def __find_function(self, command:list, flag:list): - '''Finds the name of the function that should be called - - Args: - command - the command that was given to the parser - flag - the flag that was given to the parser - - Raises: - CustomExceptions.CommandNotFoundError if the command entered is - invalid - CustomExceptions.InvalidFlagError if the flag entered is invalid - - Returns: - The name of the function that needs to be called - ''' - #convert the params to strings - command = str(command[0]) - flag = str(flag[0]) if len(flag) > 0 else "" - - #check if the list of keys in the commmand flag map contains the given command - command_list = list(self._command_flag_map.keys()) - - valid_command = command_list.__contains__(command) - if not valid_command: - raise CE.CommandNotFoundError(command) - - #pull the list of flags for the validated command - flag_list = self._command_flag_map[command] - - #Make sure that the flag is a flag (preceded with -) - #some commands are just "command arg" so this needs to be checked - prepped_flag = "" - valid_flag = True - if flag.__contains__("-"): - prepped_flag = flag.lstrip("-") - valid_flag = flag_list.__contains__(prepped_flag) - - if not valid_flag: - raise CE.InvalidFlagError(flag, command) - - #compiling the correct location to index into the function map - flag_index = flag_list.index(prepped_flag) - flags = self._command_function_map.get(command) - return flags[flag_index] - - def __find_class(self, function:str): - '''Takes a function and locates the class that it exists in - - Args: - function - the function to locate in a class - - Returns: - the class the function originates in''' - classes = [Diagram, Entity, Relation, help_menu] - for cl in classes: - if hasattr(cl, function): - return cl - diff --git a/src/umleditor/mvc_controller/uml_parser.py b/src/umleditor/mvc_controller/uml_parser.py new file mode 100644 index 00000000..1adf8d98 --- /dev/null +++ b/src/umleditor/mvc_controller/uml_parser.py @@ -0,0 +1,89 @@ +from umleditor.mvc_model import CustomExceptions as CE +from umleditor.mvc_view.cli_lexer import lex_input as lex +from umleditor.mvc_model import * + +class Parser: + def __init__(self, diagram:Diagram): + self._diagram = diagram + + def parse (self, input:str) -> list: + '''Parses a line of user input. + + Args: + input(str) - the line to be parsed + + Return: + With proper input: a list in the form [function, arg1,...,argN] + With invalid name: CustomExceptions.InvalidArgumentError + With invalid flag: CustomExceptions.InvalidFlagError + With invalid command: CustomExceptions.CommandNotFoundError + ''' + #guarding no input saves resources + if not input: + raise CE.NoInputError() + + #actual input to be parsed, split on spaces + bits = input.split() + + #Get the command that will be run + command_str = "" + #list slicing generates an empty list instead of an IndexError + command_str = lex(bits[0:1], bits[1:2]) + + #Get the args that will be passed to that command + args = [] + if not str(bits[1:2]).__contains__("-"): + args = self.__check_args(bits[1:]) + else: + args = self.__check_args(bits[2:]) + + #Get the class the command is in + command_class = self.__find_class(command_str) + + #go from knowing which class to having a specific instance + #of the class that the method needs to be called on + obj = self + if command_class == Diagram: + obj = self._diagram + elif command_class == Entity: + #if no args were provided, no entity can be found. Generate an error about invalid args + if not args: + raise CE.NoArgsGivenError() + + #if the method is in entity, get entity that needs to be changed + #pop the first element of args because it is the entity name, not a method param + obj = self._diagram.get_entity(args.pop(0)) + elif command_class == help_menu: + obj = help_menu + + #build and return the callable + args + return [getattr(obj, command_str)] + args + + def __find_class(self, function:str): + '''Takes a function and locates the class that it exists in + + Args: + function - the function to locate in a class + + Returns: + the class the function originates in''' + classes = [Diagram, Entity, Relation, help_menu] + for cl in classes: + if hasattr(cl, function): + return cl + +def check_args(self, args:list): + '''Given a list of args, checks to make sure each one is valid. + Valid is defined as alphanumeric. + + Args: + args(list): a list of strings to be checked + + Return: + CustomExceptions.InvalidArgumentError if an argument provided is invalid + The list of args provided if all args are valid + ''' + for arg in args: + if not(arg.isalnum()): + raise CE.InvalidArgumentError(arg) + return args diff --git a/src/umleditor/mvc_view/__init__.py b/src/umleditor/mvc_view/__init__.py index e69de29b..7f27a2f7 100644 --- a/src/umleditor/mvc_view/__init__.py +++ b/src/umleditor/mvc_view/__init__.py @@ -0,0 +1 @@ +from .cli_lexer import lex_input as lex \ No newline at end of file diff --git a/src/umleditor/mvc_view/cli_lexer.py b/src/umleditor/mvc_view/cli_lexer.py new file mode 100644 index 00000000..1bf7daa4 --- /dev/null +++ b/src/umleditor/mvc_view/cli_lexer.py @@ -0,0 +1,74 @@ +from umleditor.mvc_model import CustomExceptions as CE + +#map relating commands to the flags that can be passed to them +#NOTE: These must be synched with the command_function_map based on idx +_command_flag_map = { + "class" : ["a","d","r"], + "list" : ["a","c","r","d"], + "att" : ["a","d","r"], + "rel" : ["a","d"], + "save" : [""], + "load" : [""], + "exit" : [""], + "quit" : [""], + "help" : [""] +} + +#map relating commands to the names of methods that can be called on them +#NOTE: these must be synched with command_flag_map based on idx +_command_function_map = { + "class" : ["add_entity","delete_entity","rename_entity"], + "list" : ["list_everything","list_entities","list_relations","list_entity_details"], + "att" : ["add_attribute","delete_attribute","rename_attribute"], + "rel" : ["add_relation","delete_relation"], + "save" : ["save"], + "load" : ["load"], + "exit" : ["quit"], + "quit" : ["quit"], + "help" : ["help_menu"] +} + +def lex_input(self, command:list, flag:list): + '''Finds the name of the function that should be called + + Args: + command - the command that was given to the parser + flag - the flag that was given to the parser + + Raises: + CustomExceptions.CommandNotFoundError if the command entered is + invalid + CustomExceptions.InvalidFlagError if the flag entered is invalid + + Returns: + The name of the function that needs to be called + ''' + #convert the params to strings + command = str(command[0]) + flag = str(flag[0]) if len(flag) > 0 else "" + + #check if the list of keys in the commmand flag map contains the given command + command_list = list(_command_flag_map.keys()) + + valid_command = command_list.__contains__(command) + if not valid_command: + raise CE.CommandNotFoundError(command) + + #pull the list of flags for the validated command + flag_list = _command_flag_map[command] + + #Make sure that the flag is a flag (preceded with -) + #some commands are just "command arg" so this needs to be checked + prepped_flag = "" + valid_flag = True + if flag.__contains__("-"): + prepped_flag = flag.lstrip("-") + valid_flag = flag_list.__contains__(prepped_flag) + + if not valid_flag: + raise CE.InvalidFlagError(flag, command) + + #compiling the correct location to index into the function map + flag_index = flag_list.index(prepped_flag) + flags = _command_function_map.get(command) + return flags[flag_index] From 337018bda6c22061381bd9fc657b457cdf4cd38a Mon Sep 17 00:00:00 2001 From: Peter Date: Wed, 21 Feb 2024 18:03:50 -0500 Subject: [PATCH 011/144] Parser and Lexer are Split - Lexer: reads a command from user input, splits it into parseable chunks - Parser: reads the parseable chunks from the lexer, turning them into a command and arguments - method check_args is floating so that it can be used wherever needed (namely controller save and load) This passed all tests before uploading --- src/umleditor/mvc_controller/controller.py | 2 +- src/umleditor/mvc_controller/uml_parser.py | 18 ++++++++++-------- src/umleditor/mvc_view/cli_lexer.py | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/umleditor/mvc_controller/controller.py b/src/umleditor/mvc_controller/controller.py index dba2f501..9cb6adea 100644 --- a/src/umleditor/mvc_controller/controller.py +++ b/src/umleditor/mvc_controller/controller.py @@ -22,9 +22,9 @@ def __init__(self) -> None: def run(self) -> None: + p = Parser(self._diagram) while not self._should_quit: s = read_line() - p = Parser(self._diagram) try: #parse the command input = p.parse(s) diff --git a/src/umleditor/mvc_controller/uml_parser.py b/src/umleditor/mvc_controller/uml_parser.py index 1adf8d98..bcd88eb7 100644 --- a/src/umleditor/mvc_controller/uml_parser.py +++ b/src/umleditor/mvc_controller/uml_parser.py @@ -2,6 +2,8 @@ from umleditor.mvc_view.cli_lexer import lex_input as lex from umleditor.mvc_model import * +import re + class Parser: def __init__(self, diagram:Diagram): self._diagram = diagram @@ -29,13 +31,12 @@ def parse (self, input:str) -> list: command_str = "" #list slicing generates an empty list instead of an IndexError command_str = lex(bits[0:1], bits[1:2]) - #Get the args that will be passed to that command args = [] if not str(bits[1:2]).__contains__("-"): - args = self.__check_args(bits[1:]) + args = check_args(bits[1:]) else: - args = self.__check_args(bits[2:]) + args = check_args(bits[2:]) #Get the class the command is in command_class = self.__find_class(command_str) @@ -53,8 +54,8 @@ def parse (self, input:str) -> list: #if the method is in entity, get entity that needs to be changed #pop the first element of args because it is the entity name, not a method param obj = self._diagram.get_entity(args.pop(0)) - elif command_class == help_menu: - obj = help_menu + elif command_class == help_command: + obj = help_command #build and return the callable + args return [getattr(obj, command_str)] + args @@ -67,12 +68,12 @@ def __find_class(self, function:str): Returns: the class the function originates in''' - classes = [Diagram, Entity, Relation, help_menu] + classes = [Diagram, Entity, Relation, help_command] for cl in classes: if hasattr(cl, function): return cl -def check_args(self, args:list): +def check_args(args:list): '''Given a list of args, checks to make sure each one is valid. Valid is defined as alphanumeric. @@ -83,7 +84,8 @@ def check_args(self, args:list): CustomExceptions.InvalidArgumentError if an argument provided is invalid The list of args provided if all args are valid ''' + exp = re.compile('[^a-zA-Z-_0-9]') for arg in args: - if not(arg.isalnum()): + if len(exp.findall(arg)) > 0: raise CE.InvalidArgumentError(arg) return args diff --git a/src/umleditor/mvc_view/cli_lexer.py b/src/umleditor/mvc_view/cli_lexer.py index 1bf7daa4..c39fdb67 100644 --- a/src/umleditor/mvc_view/cli_lexer.py +++ b/src/umleditor/mvc_view/cli_lexer.py @@ -28,7 +28,7 @@ "help" : ["help_menu"] } -def lex_input(self, command:list, flag:list): +def lex_input(command:list, flag:list): '''Finds the name of the function that should be called Args: From a577c6b233f261f4b7585747c7ebf420ea023826 Mon Sep 17 00:00:00 2001 From: Peter Date: Wed, 21 Feb 2024 18:06:30 -0500 Subject: [PATCH 012/144] Test updates These tests no longer create a controller, and as such can no longer test controller methods. This will be fixed when everything is rewritten in pytest --- src/test/test_parseing.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/test/test_parseing.py b/src/test/test_parseing.py index b54e6318..87e3ce4c 100644 --- a/src/test/test_parseing.py +++ b/src/test/test_parseing.py @@ -5,6 +5,7 @@ from umleditor.mvc_controller.controller import Controller from umleditor.mvc_model.diagram import Diagram from umleditor.mvc_model.test import Test +from umleditor.mvc_controller.uml_parser import Parser import os # import Help @@ -13,11 +14,12 @@ from umleditor.mvc_model.relation import Relation def main(): - c = Controller() + d = Diagram() + c = Parser(d) unit_tests(c) -def unit_tests(c:Controller): +def unit_tests(c:Parser): parseTest = Test("Parser Tests", c.parse) d = c._diagram @@ -63,13 +65,6 @@ def unit_tests(c:Controller): print(parseTest.exec("relation add invalid destination", CE.InvalidArgumentError("'"), "rel -d c1 '")) print(parseTest.exec("relation invalid flag", CE.InvalidFlagError("-z", "rel"), "rel -z test")) - #Controller Method tests - print(parseTest.exec("save", [c.save], "save")) - print(parseTest.exec("load", [c.load], "load")) - print(parseTest.exec("quit", [c.quit], "quit")) - # print(parseTest.exec("help", [Help.help], "help")) - print(parseTest.exec("command invalid", CE.CommandNotFoundError("z"), "z")) - From 224660a3e7e6acaabaddc304e584c9e1f9a3f6e2 Mon Sep 17 00:00:00 2001 From: Peter Date: Wed, 21 Feb 2024 18:20:32 -0500 Subject: [PATCH 013/144] Bugfixes - Gitignore now properly ignores Linux venv directories - main no longer starts in debug mode by default --- .gitignore | 5 +++++ main.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 8591f538..a2cd30ad 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,11 @@ Include/ venv.cfg pyvenv.cfg +#Venv directories on linux +lib/ +include/ +bin/ + #Build directories and files build/ dist/ diff --git a/main.py b/main.py index 20ca957a..3b77d37e 100644 --- a/main.py +++ b/main.py @@ -21,7 +21,7 @@ def main(): print('Oh no! Unexpected Error!') if __name__ == '__main__': - if __debug__: + if not __debug__: debug_main() else: main() \ No newline at end of file From 31c2121c3fcb38faba964e5a2a0c31bd580cc29a Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 21 Feb 2024 20:22:03 -0500 Subject: [PATCH 014/144] Method and Field implemented --- src/umleditor/mvc_model/entity.py | 129 ++++++++++++++++++++++++---- src/umleditor/mvc_view/cli_lexer.py | 6 +- 2 files changed, 118 insertions(+), 17 deletions(-) diff --git a/src/umleditor/mvc_model/entity.py b/src/umleditor/mvc_model/entity.py index 745b2e53..83cd43b2 100644 --- a/src/umleditor/mvc_model/entity.py +++ b/src/umleditor/mvc_model/entity.py @@ -9,8 +9,8 @@ def __init__(self, entity_name:str=''): entity_name (str): The name of the entity. """ self.set_name(entity_name) - self._fields = [str] - self._methods = [UML_Method] + self._fields = [] + self._methods = [] def get_name(self): ''' @@ -40,6 +40,9 @@ def add_field(self, field_name:str): Raises: CustomExceptions.FieldExistsError: If the field already exists in the Entity. + + Returns: + None. """ if field_name in self._fields: raise CustomExceptions.FieldExistsError(field_name) @@ -56,6 +59,9 @@ def delete_field(self, field_name: str): Raises: CustomExceptions.FieldNotFoundError: If the specified field is not found in the entity's field list. + + Returns: + None. """ if field_name not in self._fields: raise CustomExceptions.FieldNotFoundError(field_name) @@ -75,6 +81,9 @@ def rename_field(self, old_field: str, new_field: str): not exist in the entity. CustomExceptions.FieldExistsError: If the new name is already used for another field in this entity. + + Returns: + None. """ if old_field not in self._fields: raise CustomExceptions.FieldNotFoundError(old_field) @@ -86,30 +95,69 @@ def rename_field(self, old_field: str, new_field: str): self._fields.append(new_field) def add_method(self, method_name: str): - if any(method_name == um.get_name() for um in self._methods): + """ + Adds a new method to the to the list. + + Args: + method_name (str): The method's name to be added to the entity. + + Raises: + CustomExceptions.MethodExistsError: If the method already + exists in the Entity. + Returns: + None. + """ + if any(method_name == um.get_method_name() for um in self._methods): raise CustomExceptions.MethodExistsError(method_name) else: new_method = UML_Method(method_name) self._methods.append(new_method) def delete_method(self, method_name: str): + """ + Deletes a method from this entity if the method exists. + + Args: + method_name (str): The name of the method to be deleted from the entity. + + Raises: + CustomExceptions.MethodNotFoundError: If the specified method + is not found in the entity's method list. + + Returns: + None. + """ + if not any(method_name == um.get_method_name() for um in self._methods): + raise CustomExceptions.MethodNotFoundError(method_name) for um in self._methods: - if um.get_name == method_name: + if um.get_method_name() == method_name: self._methods.remove(um) - else: - raise CustomExceptions.MethodNotFoundError(method_name) def rename_method(self, old_name: str, new_name: str): - if not any(old_name == um.get_name() for um in self._methods): + """ + Renames a method from its old name to a new name + + Args: + old_name(str): The current name of the method. + new_name (str): The new name for the method. + + Raises: + CustomExceptions.MethodNotFoundError: If the old method does + not exist in the entity. + CustomExceptions.MethodExistsError: If the new name is already + used for another method in this entity. + + Returns: + None. + """ + if not any(old_name == um.get_method_name() for um in self._methods): raise CustomExceptions.MethodNotFoundError(old_name) - elif any(new_name == um.get_name() for um in self._methods): + elif any(new_name == um.get_method_name() for um in self._methods): raise CustomExceptions.MethodExistsError(new_name) else: for um in self._methods: - if (old_name == um.get_name()): - self._methods.remove(um) - new_method = UML_Method(new_name) - self._methods.append(new_method) + if (old_name == um.get_method_name()): + um.set_method_name(new_name) def __str__(self) -> str: """ @@ -122,11 +170,62 @@ def __str__(self) -> str: class UML_Method: def __init__(self, method_name): + """ + Creates a UML_Method object. + + Args: + method_name (str): The name of the method. + + Raises: + None. + + Returns: + None. + """ self._name = method_name self._params = [] - def get_name(self): + def get_method_name(self): + """ + Returns the name of the method. + + Args: + None. + + Raises: + None. + + Returns: + name (Entity): The name of the method. + """ return self._name - def set_name(self, new_name): - self._name = new_name \ No newline at end of file + def set_method_name(self, new_name): + """ + Changes the name of the method. + + Args: + new_name (str): The new name of the method. + + Raises: + None. + + Returns: + None. + """ + self._name = new_name + + def __str__(self): + """ + Returns the name of the method. + + Args: + None. + + Raises: + None. + + Returns: + name (str): A string to represent the method. + """ + return self.get_method_name() \ No newline at end of file diff --git a/src/umleditor/mvc_view/cli_lexer.py b/src/umleditor/mvc_view/cli_lexer.py index c39fdb67..8c564295 100644 --- a/src/umleditor/mvc_view/cli_lexer.py +++ b/src/umleditor/mvc_view/cli_lexer.py @@ -5,7 +5,8 @@ _command_flag_map = { "class" : ["a","d","r"], "list" : ["a","c","r","d"], - "att" : ["a","d","r"], + "fld" : ["a","d","r"], + "mthd" : ["a","d","r"], "rel" : ["a","d"], "save" : [""], "load" : [""], @@ -19,7 +20,8 @@ _command_function_map = { "class" : ["add_entity","delete_entity","rename_entity"], "list" : ["list_everything","list_entities","list_relations","list_entity_details"], - "att" : ["add_attribute","delete_attribute","rename_attribute"], + "fld" : ["add_field","delete_field","rename_field"], + "mthd" : ["add_method","delete_method","rename_method"], "rel" : ["add_relation","delete_relation"], "save" : ["save"], "load" : ["load"], From 7fa3554e5a945e0bf886d639a904a03671a70065 Mon Sep 17 00:00:00 2001 From: almostTaklu Date: Wed, 21 Feb 2024 22:43:42 -0500 Subject: [PATCH 015/144] Added relationship type --- .gitignore | 5 ++- src/umleditor/mvc_model/custom_exceptions.py | 13 +++++++ src/umleditor/mvc_model/diagram.py | 38 ++++++++++++++++++-- src/umleditor/mvc_model/relation.py | 7 ++-- src/umleditor/mvc_view/cli_lexer.py | 4 +-- 5 files changed, 60 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index a6b6ddc1..ab8a3e31 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,7 @@ dist/ main.spec #pytest -.pytest_cache \ No newline at end of file +.pytest_cache + +#Macos specifics +.DS_Store \ No newline at end of file diff --git a/src/umleditor/mvc_model/custom_exceptions.py b/src/umleditor/mvc_model/custom_exceptions.py index 120fc1d0..56d66ae5 100644 --- a/src/umleditor/mvc_model/custom_exceptions.py +++ b/src/umleditor/mvc_model/custom_exceptions.py @@ -76,6 +76,19 @@ class RelationDoesNotExistError(Error): def __init__(self, source, destination): super().__init__(f"Relation between '{source} -> {destination}' does not exist.") + class InvalidRelationTypeError(Error): + """ + Exception raised when the relation being added has no types. + + Args: + source (Entity): The source of the relation that was being added. + destination (Entity): The destination of the relation that was + being added. + + """ + def __init__(self, source, destination): + super().__init__(f"Relation between '{source} -> {destination}' has no types.") + #===============================================================================# #Parser/Controller Exceptions #===============================================================================# diff --git a/src/umleditor/mvc_model/diagram.py b/src/umleditor/mvc_model/diagram.py index 988dbcf4..5607e40d 100644 --- a/src/umleditor/mvc_model/diagram.py +++ b/src/umleditor/mvc_model/diagram.py @@ -166,20 +166,27 @@ def list_relations(self): return '\n'.join(relations_list) - def add_relation(self, source, destination): + def add_relation(self,source, destination, type): """ Adds a relation between two Entities. Args: source (str): The name of the source entity. destination (str): The name of the destination entity. + type (str): The type of the relation. Raises: CustomExceptions.EntityNotFoundError: If either the source or the destination entity are not found. CustomExceptions.RelationExistsError: If a relation already exists between the source and destination entities. + CustomExceptions.InvalidRelationTypeError: If the relation type is + not valid. """ + # Check for valid relationship type + if type not in Relation.RELATIONSHIP_TYPE: + raise CustomExceptions.InvalidRelationTypeError(type) + # Check for valid source and destination if source not in self._entities: raise CustomExceptions.EntityNotFoundError(source) @@ -191,7 +198,7 @@ def add_relation(self, source, destination): if rel.get_source() == self._entities[source] and rel.get_destination() == self._entities[destination]: raise CustomExceptions.RelationExistsError(source, destination) # Pass entity objects to relation and add relation to list of existing relations - relationship = Relation(self._entities[source], self._entities[destination]) + relationship = Relation(type, self._entities[source], self._entities[destination]) self._relations.append(relationship) def delete_relation(self, source, destination): @@ -221,3 +228,30 @@ def delete_relation(self, source, destination): return raise CustomExceptions.RelationDoesNotExistError(source, destination) + def change_relation_type(self, source, destination, new_type): + """ + Changes the type of a relation between two Entities. + + Args: + source (str): The name of the source entity. + destination (str): The name of the destination entity. + new_type (str): The new type of the relation. + + Raises: + CustomExceptions.EntityNotFoundError: If either the source or the + destination entity is not found. + CustomExceptions.RelationDoesNotExistError: If a relation does not + exist between the source and destination entities. + CustomExceptions.InvalidRelationTypeError: If the relation type is + not valid. + """ + # Check for valid relationship type + if new_type not in Relation.RELATIONSHIP_TYPE: + raise CustomExceptions.InvalidRelationTypeError(new_type) + + for rel in self._relations: + if rel.get_source() == self._entities[source] and rel.get_destination() == self._entities[destination]: + rel._type = new_type + return + raise CustomExceptions.RelationDoesNotExistError(source, destination) + diff --git a/src/umleditor/mvc_model/relation.py b/src/umleditor/mvc_model/relation.py index d0a18a41..2e5899be 100644 --- a/src/umleditor/mvc_model/relation.py +++ b/src/umleditor/mvc_model/relation.py @@ -1,7 +1,9 @@ from .entity import Entity class Relation: - def __init__(self, source=Entity(), destination=Entity()): + RELATIONSHIP_TYPE = {'aggregation', 'composition', 'inheritance', 'realization'} + + def __init__(self, type, source=Entity(), destination=Entity()): """ Creates a relation between a source entity to a destination entity. @@ -17,6 +19,7 @@ def __init__(self, source=Entity(), destination=Entity()): """ self._source = source self._destination = destination + self._type = type def get_source(self): """ @@ -77,4 +80,4 @@ def __str__(self): Returns: str: A string representation of the relation. """ - return f'{self._source} -> {self._destination}' + return f'{self._source} -> {self._type} -> {self._destination}' diff --git a/src/umleditor/mvc_view/cli_lexer.py b/src/umleditor/mvc_view/cli_lexer.py index c39fdb67..79c63d4b 100644 --- a/src/umleditor/mvc_view/cli_lexer.py +++ b/src/umleditor/mvc_view/cli_lexer.py @@ -6,7 +6,7 @@ "class" : ["a","d","r"], "list" : ["a","c","r","d"], "att" : ["a","d","r"], - "rel" : ["a","d"], + "rel" : ["a","d", "t"], "save" : [""], "load" : [""], "exit" : [""], @@ -20,7 +20,7 @@ "class" : ["add_entity","delete_entity","rename_entity"], "list" : ["list_everything","list_entities","list_relations","list_entity_details"], "att" : ["add_attribute","delete_attribute","rename_attribute"], - "rel" : ["add_relation","delete_relation"], + "rel" : ["add_relation","delete_relation", "change_relation_type"], "save" : ["save"], "load" : ["load"], "exit" : ["quit"], From e2bfebdcff1f1c59e11317a0bbe81a3bd5bfc91e Mon Sep 17 00:00:00 2001 From: Marshall Feng Date: Wed, 21 Feb 2024 23:31:27 -0500 Subject: [PATCH 016/144] Add Exceptions for parameter operations --- src/umleditor/mvc_model/custom_exceptions.py | 24 ++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/umleditor/mvc_model/custom_exceptions.py b/src/umleditor/mvc_model/custom_exceptions.py index 9c4474f0..e20946b8 100644 --- a/src/umleditor/mvc_model/custom_exceptions.py +++ b/src/umleditor/mvc_model/custom_exceptions.py @@ -98,6 +98,30 @@ class MethodNotFoundError(Error): """ def __init__(self, method_name): super().__init__(f"Method named: '{method_name}' does not exist.") + + #===============================================================================# + #Parameter Exceptions + #===============================================================================# + class ParameterExistsError(Error): + """ + Exception raised when the parameter already exists. + + Args: + parameter_name (str): The name of the parameter that exists. + """ + def __init__(self, parameter_name): + super().__init__(f"Parameter named: '{parameter_name}' already exists.") + + class ParameterNotFoundError(Error): + """ + Exception raised when the parameter was not found. + + Args: + parameter_name (str): The name of the parameter that does not exist. + """ + def __init__(self, parameter_name): + super().__init__(f"Parameter named: '{parameter_name}' does not exist.") + #===============================================================================# #Parser/Controller Exceptions #===============================================================================# From 435dab31b05d11edf1b74c19078f61bacadd8ad1 Mon Sep 17 00:00:00 2001 From: Marshall Feng Date: Wed, 21 Feb 2024 23:32:03 -0500 Subject: [PATCH 017/144] Add add/remove/change operations for parameter --- src/umleditor/mvc_model/entity.py | 66 +++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/src/umleditor/mvc_model/entity.py b/src/umleditor/mvc_model/entity.py index 83cd43b2..3c67c7ee 100644 --- a/src/umleditor/mvc_model/entity.py +++ b/src/umleditor/mvc_model/entity.py @@ -215,6 +215,72 @@ def set_method_name(self, new_name): """ self._name = new_name + def add_parameters(self, params: list[str]): + """ + Adds a list of new parameters to the method. + + Args: + params (list[str]): The list of new parameters to be added. + + Raises: + CustomExceptions.ParameterExistsError: If any of the parameter already exists in the method. + + Returns: + None. + """ + for new_param in params: + if new_param in self._params: + raise CustomExceptions.ParameterExistsError(new_param) + self._params.extend(params) + + def remove_parameters(self, params: list[str]): + """ + Removes a list of parameters from the method. + + Args: + params (list[str]): The list of parameters to be removed. + + Raises: + CustomExceptions.ParameterNotFoundError: If any of the parameter does not exist in the method. + + Returns: + None. + """ + for remove_param in params: + if remove_param not in self._params: + raise CustomExceptions.ParameterNotFoundError(remove_param) + for remove_param in params: + del self._params[self._params.index(remove_param)] + + def change_parameters(self, old_params: list[str], new_params: list[str]): + """ + Changes a list of parameters to a new list of parameters to the method. + + Args: + old_params (list[str]): The list of parameters to be removed. + new_params (list[str]): The list of new parameters to be added. + + Raises: + CustomExceptions.ParameterNotFoundError: If any of the parameter does not exist in the method. + CustomExceptions.ParameterExistsError: If any of the parameter already exists in the method. + + Returns: + None. + """ + for remove_param in old_params: + if remove_param not in self._params: + raise CustomExceptions.ParameterNotFoundError(remove_param) + deleted_params = [] + for remove_param in old_params: + idx = self._params.index(remove_param) + deleted_params.append(self._params[idx]) + del self._params[idx] + for new_param in new_params: + if new_param in self._params: + self._params.extend(deleted_params) # restore all deleted parameters if add operation should fail (Order not preserved) + raise CustomExceptions.ParameterExistsError(new_param) + self._params.extend(new_params) + def __str__(self): """ Returns the name of the method. From ee26560587c63f60b591ba8f238de043fe11aca4 Mon Sep 17 00:00:00 2001 From: Marshall Feng Date: Wed, 21 Feb 2024 23:39:00 -0500 Subject: [PATCH 018/144] Change method documentation --- src/umleditor/mvc_model/entity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/umleditor/mvc_model/entity.py b/src/umleditor/mvc_model/entity.py index 3c67c7ee..7529caeb 100644 --- a/src/umleditor/mvc_model/entity.py +++ b/src/umleditor/mvc_model/entity.py @@ -261,8 +261,8 @@ def change_parameters(self, old_params: list[str], new_params: list[str]): new_params (list[str]): The list of new parameters to be added. Raises: - CustomExceptions.ParameterNotFoundError: If any of the parameter does not exist in the method. - CustomExceptions.ParameterExistsError: If any of the parameter already exists in the method. + CustomExceptions.ParameterNotFoundError: If any of the parameter to be removed does not exist in the method. + CustomExceptions.ParameterExistsError: If any of the parameter to be added already exists in the method. Returns: None. From 5dd5e5709265ce7e6d11ddb4bd2b4db5e294e107 Mon Sep 17 00:00:00 2001 From: Marshall Feng Date: Wed, 21 Feb 2024 23:49:36 -0500 Subject: [PATCH 019/144] Optimized restore process to preserve order if change operation should fail --- src/umleditor/mvc_model/entity.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/umleditor/mvc_model/entity.py b/src/umleditor/mvc_model/entity.py index 7529caeb..74001bf3 100644 --- a/src/umleditor/mvc_model/entity.py +++ b/src/umleditor/mvc_model/entity.py @@ -273,11 +273,13 @@ def change_parameters(self, old_params: list[str], new_params: list[str]): deleted_params = [] for remove_param in old_params: idx = self._params.index(remove_param) - deleted_params.append(self._params[idx]) + deleted_params.append([idx, self._params[idx]]) del self._params[idx] for new_param in new_params: if new_param in self._params: - self._params.extend(deleted_params) # restore all deleted parameters if add operation should fail (Order not preserved) + # restore all deleted parameters if add operation should fail + for idx, deleted_param in reversed(deleted_params): + self._params.insert(idx, deleted_param) raise CustomExceptions.ParameterExistsError(new_param) self._params.extend(new_params) From 41b0c33d2c583c81af6a38b91636040ff04b0e94 Mon Sep 17 00:00:00 2001 From: Peter F Date: Thu, 22 Feb 2024 19:05:38 -0500 Subject: [PATCH 020/144] Parser and Lexer updates - Parser now handles save, load, and quit again - Parser is no longer its own object - Parser now takes an instance of the controller which it uses to generate instance-specific command objects - Lexer is now back in controller - Lexer is being used for all views, and has been renamed accordingly --- src/umleditor/mvc_controller/controller.py | 11 +- .../uml_lexer.py} | 0 src/umleditor/mvc_controller/uml_parser.py | 124 +++++++++--------- 3 files changed, 64 insertions(+), 71 deletions(-) rename src/umleditor/{mvc_view/cli_lexer.py => mvc_controller/uml_lexer.py} (100%) diff --git a/src/umleditor/mvc_controller/controller.py b/src/umleditor/mvc_controller/controller.py index 9cb6adea..e4c48d0d 100644 --- a/src/umleditor/mvc_controller/controller.py +++ b/src/umleditor/mvc_controller/controller.py @@ -1,6 +1,6 @@ from .controller_input import read_file, read_line import umleditor.mvc_controller.controller_output as controller_output -from umleditor.mvc_controller.uml_parser import Parser +from umleditor.mvc_controller.uml_parser import parse from .serializer import CustomJSONEncoder, serialize, deserialize from umleditor.mvc_model import CustomExceptions as CE from umleditor.mvc_model.diagram import Diagram @@ -8,12 +8,6 @@ from umleditor.mvc_controller.uml_parser import check_args import os -#Parser Includes. These will be moved out when the parser is moved. -from umleditor.mvc_model.entity import Entity -from umleditor.mvc_model.relation import Relation - - - class Controller: def __init__(self) -> None: @@ -22,12 +16,11 @@ def __init__(self) -> None: def run(self) -> None: - p = Parser(self._diagram) while not self._should_quit: s = read_line() try: #parse the command - input = p.parse(s) + input = parse(self, s) #return from input is [function object, arg1,...,argn] command = input[0] diff --git a/src/umleditor/mvc_view/cli_lexer.py b/src/umleditor/mvc_controller/uml_lexer.py similarity index 100% rename from src/umleditor/mvc_view/cli_lexer.py rename to src/umleditor/mvc_controller/uml_lexer.py diff --git a/src/umleditor/mvc_controller/uml_parser.py b/src/umleditor/mvc_controller/uml_parser.py index bcd88eb7..e98651d5 100644 --- a/src/umleditor/mvc_controller/uml_parser.py +++ b/src/umleditor/mvc_controller/uml_parser.py @@ -1,77 +1,77 @@ from umleditor.mvc_model import CustomExceptions as CE -from umleditor.mvc_view.cli_lexer import lex_input as lex +from .uml_lexer import lex_input as lex from umleditor.mvc_model import * import re -class Parser: - def __init__(self, diagram:Diagram): - self._diagram = diagram +#list of all classes that need to be searched for commands +classes = [Diagram, Entity, Relation, help_command] - def parse (self, input:str) -> list: - '''Parses a line of user input. - - Args: - input(str) - the line to be parsed - - Return: - With proper input: a list in the form [function, arg1,...,argN] - With invalid name: CustomExceptions.InvalidArgumentError - With invalid flag: CustomExceptions.InvalidFlagError - With invalid command: CustomExceptions.CommandNotFoundError - ''' - #guarding no input saves resources - if not input: - raise CE.NoInputError() + +def parse (c, input:str) -> list: + '''Parses a line of user input. + + Args: + c - the controller that owns the diagram being modified + input(str) - the line to be parsed - #actual input to be parsed, split on spaces - bits = input.split() + Return: + With proper input: a list in the form [function, arg1,...,argN] + With invalid name: CustomExceptions.InvalidArgumentError + With invalid flag: CustomExceptions.InvalidFlagError + With invalid command: CustomExceptions.CommandNotFoundError + ''' + #guarding no input saves resources + if not input: + raise CE.NoInputError() + + #actual input to be parsed, split on spaces + bits = input.split() - #Get the command that will be run - command_str = "" - #list slicing generates an empty list instead of an IndexError - command_str = lex(bits[0:1], bits[1:2]) - #Get the args that will be passed to that command - args = [] - if not str(bits[1:2]).__contains__("-"): - args = check_args(bits[1:]) - else: - args = check_args(bits[2:]) + #Get the command that will be run + command_str = "" + #list slicing generates an empty list instead of an IndexError + command_str = lex(bits[0:1], bits[1:2]) + #Get the args that will be passed to that command + args = [] + if not str(bits[1:2]).__contains__("-"): + args = check_args(bits[1:]) + else: + args = check_args(bits[2:]) - #Get the class the command is in - command_class = self.__find_class(command_str) + #Get the class the command is in + command_class = __find_class(command_str) - #go from knowing which class to having a specific instance - #of the class that the method needs to be called on - obj = self - if command_class == Diagram: - obj = self._diagram - elif command_class == Entity: - #if no args were provided, no entity can be found. Generate an error about invalid args - if not args: - raise CE.NoArgsGivenError() - - #if the method is in entity, get entity that needs to be changed - #pop the first element of args because it is the entity name, not a method param - obj = self._diagram.get_entity(args.pop(0)) - elif command_class == help_command: - obj = help_command + #go from knowing which class to having a specific instance + #of the class that the method needs to be called on + obj = c + if command_class == Diagram: + obj = c._diagram + elif command_class == Entity: + #if no args were provided, no entity can be found. Generate an error about invalid args + if not args: + raise CE.NoArgsGivenError() - #build and return the callable + args - return [getattr(obj, command_str)] + args + #if the method is in entity, get entity that needs to be changed + #pop the first element of args because it is the entity name, not a method param + obj = c._diagram.get_entity(args.pop(0)) + elif command_class == help_command: + obj = help_command + + #build and return the callable + args + return [getattr(obj, command_str)] + args - def __find_class(self, function:str): - '''Takes a function and locates the class that it exists in - - Args: - function - the function to locate in a class - - Returns: - the class the function originates in''' - classes = [Diagram, Entity, Relation, help_command] - for cl in classes: - if hasattr(cl, function): - return cl +def __find_class(function:str): + '''Takes a function and locates the class that it exists in + + Args: + function - the function to locate in a class + + Returns: + the class the function originates in''' + for cl in classes: + if hasattr(cl, function): + return cl def check_args(args:list): '''Given a list of args, checks to make sure each one is valid. From 05785e72022371745225316327fd5b10897c5015 Mon Sep 17 00:00:00 2001 From: AdamG-L Date: Thu, 22 Feb 2024 19:19:55 -0500 Subject: [PATCH 021/144] Moving GUI to main project --- .../mvc_controller/controller_GUI.py | 31 +++++++ src/umleditor/mvc_view/gui_view/__init__.py | 3 + src/umleditor/mvc_view/gui_view/class_card.py | 36 ++++++++ .../mvc_view/gui_view/class_input_dialog.py | 24 +++++ src/umleditor/mvc_view/gui_view/uml.ui | 91 +++++++++++++++++++ src/umleditor/mvc_view/gui_view/view_GUI.py | 46 ++++++++++ 6 files changed, 231 insertions(+) create mode 100644 src/umleditor/mvc_controller/controller_GUI.py create mode 100644 src/umleditor/mvc_view/gui_view/__init__.py create mode 100644 src/umleditor/mvc_view/gui_view/class_card.py create mode 100644 src/umleditor/mvc_view/gui_view/class_input_dialog.py create mode 100644 src/umleditor/mvc_view/gui_view/uml.ui create mode 100644 src/umleditor/mvc_view/gui_view/view_GUI.py diff --git a/src/umleditor/mvc_controller/controller_GUI.py b/src/umleditor/mvc_controller/controller_GUI.py new file mode 100644 index 00000000..38fdce94 --- /dev/null +++ b/src/umleditor/mvc_controller/controller_GUI.py @@ -0,0 +1,31 @@ +from umleditor.mvc_view.gui_view.view_GUI import ViewGUI +from PyQt6 import QtWidgets +from PyQt6.QtWidgets import QInputDialog, QLineEdit +from PyQt6.QtCore import QDir + + +class ControllerGUI: + + def __init__(self, window: ViewGUI) -> None: + self._window = window + # Used for detecting when tasks need run + self._window.get_signal().connect(self.run) + + def run(self, task: str): + if "class -a" in task: + self.run_add_class(task) + # Parse / Run Command + # If success, return true + # Else, create error messagebox and return false + + def run_add_class(self, task): + #TODO Parse + success = True + warning = "Invalid class name!" + print(task) + if success: + self._window.close_class_dialog() + self._window.add_class_card() + else: + self._window.invalid_input_message(warning) + diff --git a/src/umleditor/mvc_view/gui_view/__init__.py b/src/umleditor/mvc_view/gui_view/__init__.py new file mode 100644 index 00000000..d3abaf92 --- /dev/null +++ b/src/umleditor/mvc_view/gui_view/__init__.py @@ -0,0 +1,3 @@ +from .view_GUI import ViewGUI +from .class_card import ClassCard +from .class_input_dialog import ClassInputDialog \ No newline at end of file diff --git a/src/umleditor/mvc_view/gui_view/class_card.py b/src/umleditor/mvc_view/gui_view/class_card.py new file mode 100644 index 00000000..598110b0 --- /dev/null +++ b/src/umleditor/mvc_view/gui_view/class_card.py @@ -0,0 +1,36 @@ +from PyQt6.QtWidgets import QWidget, QVBoxLayout, QListWidget, QMenu, QLineEdit +from PyQt6.QtGui import QAction +from PyQt6.QtCore import Qt + +class ClassCard(QWidget): + def __init__(self): + super().__init__() + self.initUI() + + def initUI(self): + self.layout = QVBoxLayout() + + # Create list widget + self.list_widget = QListWidget() + self.list_widget.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + self.list_widget.customContextMenuRequested.connect(self.showContextMenu) + self.layout.addWidget(self.list_widget) + + self.setLayout(self.layout) + + # Add some initial items to the list + for i in range(5): + self.list_widget.addItem(f"Item {i}") + + def showContextMenu(self, position): + item = self.list_widget.itemAt(position) + if item is not None: + menu = QMenu() + edit_action = QAction("Edit", self) + edit_action.triggered.connect(lambda: self.editItem(item)) + menu.addAction(edit_action) + menu.exec(self.list_widget.mapToGlobal(position)) + + def editItem(self, item): + index = self.list_widget.row(item) + print(item.text()) \ No newline at end of file diff --git a/src/umleditor/mvc_view/gui_view/class_input_dialog.py b/src/umleditor/mvc_view/gui_view/class_input_dialog.py new file mode 100644 index 00000000..91105fc3 --- /dev/null +++ b/src/umleditor/mvc_view/gui_view/class_input_dialog.py @@ -0,0 +1,24 @@ +from PyQt6.QtWidgets import QPushButton, QVBoxLayout, QLineEdit, QDialog, QDialogButtonBox, QLabel +''' + Custom Dialog Box w/ custom accept signal allowing us to + check for valid class input +''' +class ClassInputDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Add Class") + + self.input_text = QLineEdit() + self.ok_button = QPushButton("OK") + self.cancel_button = QPushButton("Cancel") + + button_box = QDialogButtonBox() + button_box.addButton(self.ok_button, QDialogButtonBox.ButtonRole.AcceptRole) + button_box.addButton(self.cancel_button, QDialogButtonBox.ButtonRole.RejectRole) + + layout = QVBoxLayout() + layout.addWidget(self.input_text) + layout.addWidget(button_box) + self.setLayout(layout) + + self.cancel_button.clicked.connect(self.reject) \ No newline at end of file diff --git a/src/umleditor/mvc_view/gui_view/uml.ui b/src/umleditor/mvc_view/gui_view/uml.ui new file mode 100644 index 00000000..a5b66fc4 --- /dev/null +++ b/src/umleditor/mvc_view/gui_view/uml.ui @@ -0,0 +1,91 @@ + + + MainWindow + + + + 0 + 0 + 800 + 600 + + + + MainWindow + + + + + + -1 + -1 + 801 + 571 + + + + + + + + + 0 + 0 + 800 + 22 + + + + + File + + + + + + + Edit + + + + + + View + + + + + + + + + + + + Save + + + + + Load + + + + + Add Class + + + + + List + + + + + Help + + + + + + diff --git a/src/umleditor/mvc_view/gui_view/view_GUI.py b/src/umleditor/mvc_view/gui_view/view_GUI.py new file mode 100644 index 00000000..41a170fb --- /dev/null +++ b/src/umleditor/mvc_view/gui_view/view_GUI.py @@ -0,0 +1,46 @@ +import sys, os +from PyQt6 import QtCore, QtGui, QtWidgets +from PyQt6 import uic +from PyQt6.QtWidgets import QMessageBox, QWidget, QVBoxLayout +from PyQt6.QtCore import pyqtSignal +from umleditor.mvc_view.gui_view.class_input_dialog import ClassInputDialog +from umleditor.mvc_view.gui_view.class_card import ClassCard + +class ViewGUI(QtWidgets.QMainWindow): + # Signal triggered for task processing + _process_task_signal = pyqtSignal(str) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + print(os.path.dirname(__file__)) + self._ui = uic.loadUi(os.path.join(os.path.dirname(__file__),"uml.ui"), self) + print("Potato") + self.connect_menu() + + def get_signal(self): + return self._process_task_signal + + def connect_menu(self): + self._ui.actionAdd_Class.triggered.connect(self.add_class_click) + + def invalid_input_message(self, warning: str): + QMessageBox.critical(self, "Error", warning) + + ############# Add Class Methods ############ + def add_class_click(self): + self._dialog = ClassInputDialog() + self._dialog.ok_button.clicked.connect(self.confirm_class_clicked) + self._dialog.exec() + + def confirm_class_clicked(self): + task = 'class -a ' + self._dialog.input_text.text() + # Emit signal to controller to handle task + self._process_task_signal.emit(task) + + def close_class_dialog(self): + self._dialog.reject() + + def add_class_card(self): + self._class_card = ClassCard() + # Add the custom widget to the central widget of the main window + self._ui.gridLayout.addWidget(self._class_card, 0, 0) \ No newline at end of file From e65e6ef5d5f3b8803026a91645f7eb56e088505a Mon Sep 17 00:00:00 2001 From: AdamG-L Date: Thu, 22 Feb 2024 19:35:53 -0500 Subject: [PATCH 022/144] removing init issue --- main.py | 29 +++++++++++++++++-- .../mvc_controller/controller_GUI.py | 4 ++- src/umleditor/mvc_view/__init__.py | 1 - 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/main.py b/main.py index 3b77d37e..6972302b 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,8 @@ -# from Controller import Controller -# from umleditor.mvc_controller.controller import Controller from umleditor.mvc_controller import Controller +from umleditor.mvc_view.gui_view.view_GUI import ViewGUI +from umleditor.mvc_controller.controller_GUI import ControllerGUI +import sys +from PyQt6 import QtWidgets def debug_main(): app = Controller() @@ -20,8 +22,29 @@ def main(): # Never expect errors to be caught here print('Oh no! Unexpected Error!') +def mainGUI(): + try: + # Create QApplication for running the program + app = QtWidgets.QApplication(sys.argv) + # Create an instance of our view and pass to controller + mainWindow = ViewGUI() + controller = ControllerGUI(mainWindow) + # Set window to visible and start the application + mainWindow.show() + app.exec() + except KeyboardInterrupt: + # This handles ctrl+C + pass + except EOFError: + # This handles ctrl+D + pass + except Exception as e: + # Never expect errors to be caught here + print('Oh no! Unexpected Error!') + if __name__ == '__main__': if not __debug__: debug_main() else: - main() \ No newline at end of file + #main() + mainGUI() \ No newline at end of file diff --git a/src/umleditor/mvc_controller/controller_GUI.py b/src/umleditor/mvc_controller/controller_GUI.py index 38fdce94..7b99f1cb 100644 --- a/src/umleditor/mvc_controller/controller_GUI.py +++ b/src/umleditor/mvc_controller/controller_GUI.py @@ -1,4 +1,5 @@ from umleditor.mvc_view.gui_view.view_GUI import ViewGUI +from umleditor.mvc_model.diagram import Diagram from PyQt6 import QtWidgets from PyQt6.QtWidgets import QInputDialog, QLineEdit from PyQt6.QtCore import QDir @@ -8,6 +9,7 @@ class ControllerGUI: def __init__(self, window: ViewGUI) -> None: self._window = window + self._diagram = Diagram() # Used for detecting when tasks need run self._window.get_signal().connect(self.run) @@ -20,7 +22,7 @@ def run(self, task: str): def run_add_class(self, task): #TODO Parse - success = True + success = False warning = "Invalid class name!" print(task) if success: diff --git a/src/umleditor/mvc_view/__init__.py b/src/umleditor/mvc_view/__init__.py index 7f27a2f7..e69de29b 100644 --- a/src/umleditor/mvc_view/__init__.py +++ b/src/umleditor/mvc_view/__init__.py @@ -1 +0,0 @@ -from .cli_lexer import lex_input as lex \ No newline at end of file From de1b57677c908bd733f06e514a265a19c52f5006 Mon Sep 17 00:00:00 2001 From: AdamG-L Date: Thu, 22 Feb 2024 19:36:18 -0500 Subject: [PATCH 023/144] remove potato --- src/umleditor/mvc_view/gui_view/view_GUI.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/umleditor/mvc_view/gui_view/view_GUI.py b/src/umleditor/mvc_view/gui_view/view_GUI.py index 41a170fb..649c0b0d 100644 --- a/src/umleditor/mvc_view/gui_view/view_GUI.py +++ b/src/umleditor/mvc_view/gui_view/view_GUI.py @@ -14,7 +14,6 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) print(os.path.dirname(__file__)) self._ui = uic.loadUi(os.path.join(os.path.dirname(__file__),"uml.ui"), self) - print("Potato") self.connect_menu() def get_signal(self): From 474d12e53b758e0fa9909b2616f2aeb460bbc492 Mon Sep 17 00:00:00 2001 From: Peter Freedman <104784188+pwfreedm@users.noreply.github.com> Date: Thu, 22 Feb 2024 19:39:32 -0500 Subject: [PATCH 024/144] Update main.py --- main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index 6972302b..184f7690 100644 --- a/main.py +++ b/main.py @@ -46,5 +46,5 @@ def mainGUI(): if not __debug__: debug_main() else: - #main() - mainGUI() \ No newline at end of file + main() + #mainGUI() From 99990e210d01f175893e91de5360091765bcdc06 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 22 Feb 2024 19:45:53 -0500 Subject: [PATCH 025/144] Test for basic functionality of existing things. --- src/test/test_entity.py | 128 ++++++++++++++++++++++++++++++++++++ src/test/test_imports.py | 41 ++++++++++-- src/test/test_relation.py | 26 ++++++++ src/test/test_uml_method.py | 21 ++++++ 4 files changed, 212 insertions(+), 4 deletions(-) create mode 100644 src/test/test_entity.py create mode 100644 src/test/test_relation.py create mode 100644 src/test/test_uml_method.py diff --git a/src/test/test_entity.py b/src/test/test_entity.py new file mode 100644 index 00000000..36f877e6 --- /dev/null +++ b/src/test/test_entity.py @@ -0,0 +1,128 @@ +from umleditor.mvc_model import Entity + +def test_create_entity(): + ent1 = Entity("entity1") + assert ent1 + +def test_get_name(): + ent1 = Entity("entity1") + assert ent1.get_name() == "entity1" + assert ent1.get_name() != "entity2" + +def test_set_name(): + ent1 = Entity("entity1") + assert ent1.get_name() == "entity1" + ent1.set_name("entity2") + assert ent1.get_name() != "entity1" + assert ent1.get_name() == "entity2" + +def test_add_field(): + ent1 = Entity("entity1") + assert "field1" not in ent1._fields + ent1.add_field("field1") + assert "field1" in ent1._fields + +def test_add_multiple_fields(): + ent1 = Entity("entity1") + ent1.add_field("field1") + ent1.add_field("field2") + ent1.add_field("field3") + assert "field1" in ent1._fields + assert "field2" in ent1._fields + assert "field3" in ent1._fields + assert "field4" not in ent1._fields + +def test_delete_field(): + ent1 = Entity("entity1") + assert "field1" not in ent1._fields + ent1.add_field("field1") + assert "field1" in ent1._fields + ent1.delete_field("field1") + assert "field1" not in ent1._fields + +def test_delete_multiple_fields(): + ent1 = Entity("entity1") + ent1.add_field("field1") + ent1.add_field("field2") + ent1.add_field("field3") + ent1.add_field("field4") + assert "field1" in ent1._fields + assert "field2" in ent1._fields + assert "field3" in ent1._fields + assert "field4" in ent1._fields + ent1.delete_field("field1") + ent1.delete_field("field2") + ent1.delete_field("field3") + assert "field1" not in ent1._fields + assert "field2" not in ent1._fields + assert "field3" not in ent1._fields + assert "field4" in ent1._fields + +def test_rename_field(): + ent1 = Entity("entity1") + assert "field1" not in ent1._fields + ent1.add_field("field1") + assert "field1" in ent1._fields + assert "field2" not in ent1._fields + ent1.rename_field("field1", "field2") + assert "field1" not in ent1._fields + assert "field2" in ent1._fields + +def test_rename_multiple_fields(): + ent1 = Entity("entity1") + ent1.add_field("field1") + ent1.add_field("field2") + assert "field1" in ent1._fields + assert "field2" in ent1._fields + assert "field3" not in ent1._fields + assert "field4" not in ent1._fields + ent1.rename_field("field1", "field3") + ent1.rename_field("field2", "field4") + assert "field1" not in ent1._fields + assert "field2" not in ent1._fields + assert "field3" in ent1._fields + assert "field4" in ent1._fields + +def test_add_method(): + ent1 = Entity("entity1") + assert not any("method1" == um.get_method_name() for um in ent1._methods) + ent1.add_method("method1") + assert any("method1" == um.get_method_name() for um in ent1._methods) + +def test_add_mutliple_methods(): + ent1 = Entity("entity1") + assert not any("method1" == um.get_method_name() for um in ent1._methods) + assert not any("method2" == um.get_method_name() for um in ent1._methods) + assert not any("method3" == um.get_method_name() for um in ent1._methods) + assert not any("method4" == um.get_method_name() for um in ent1._methods) + ent1.add_method("method1") + ent1.add_method("method2") + ent1.add_method("method3") + assert any("method1" == um.get_method_name() for um in ent1._methods) + assert any("method2" == um.get_method_name() for um in ent1._methods) + assert any("method3" == um.get_method_name() for um in ent1._methods) + assert not any("method4" == um.get_method_name() for um in ent1._methods) + +def test_rename_method(): + ent1 = Entity("entity1") + ent1.add_method("method1") + assert any("method1" == um.get_method_name() for um in ent1._methods) + assert not any("method2" == um.get_method_name() for um in ent1._methods) + ent1.rename_method("method1", "method2") + assert not any("method1" == um.get_method_name() for um in ent1._methods) + assert any("method2" == um.get_method_name() for um in ent1._methods) + +def test_rename_multiple_methods(): + ent1 = Entity("entity1") + ent1.add_method("method1") + ent1.add_method("method2") + assert any("method1" == um.get_method_name() for um in ent1._methods) + assert any("method2" == um.get_method_name() for um in ent1._methods) + assert not any("method3" == um.get_method_name() for um in ent1._methods) + assert not any("method4" == um.get_method_name() for um in ent1._methods) + ent1.rename_method("method1", "method3") + ent1.rename_method("method2", "method4") + assert not any("method1" == um.get_method_name() for um in ent1._methods) + assert not any("method2" == um.get_method_name() for um in ent1._methods) + assert any("method3" == um.get_method_name() for um in ent1._methods) + assert any("method4" == um.get_method_name() for um in ent1._methods) \ No newline at end of file diff --git a/src/test/test_imports.py b/src/test/test_imports.py index d3b2f811..d74b7ca9 100644 --- a/src/test/test_imports.py +++ b/src/test/test_imports.py @@ -1,19 +1,52 @@ def test_import_controller(): from umleditor.mvc_controller import Controller - assert Controller def test_import_diagram(): from umleditor.mvc_model import Diagram - assert Diagram def test_import_entity(): from umleditor.mvc_model import Entity - assert Entity def test_import_relation(): from umleditor.mvc_model import Relation + assert Relation + +def test_import_method(): + from umleditor.mvc_model import UML_Method + assert UML_Method + +def test_controller_input_imports(): + from umleditor.mvc_controller.controller_input import read_line, read_file + assert read_line + assert read_file + +def test_controller_output_imports(): + from umleditor.mvc_controller.controller_output import write, write_file + assert write + assert write_file + +def test_serialzer_imports(): + from umleditor.mvc_controller.serializer import CustomJSONEncoder, serialize, deserialize + assert CustomJSONEncoder + assert serialize + assert deserialize + +def test_import_parser(): + from umleditor.mvc_controller.uml_parser import Parser + assert Parser + +def test_custom_exceptions_import(): + from umleditor.mvc_model.custom_exceptions import CustomExceptions + assert CustomExceptions + +def test_help_import(): + from umleditor.mvc_model.help_command import help_menu + assert help_menu - assert Relation \ No newline at end of file +def test_cli_lexer_import(): + from umleditor.mvc_view.cli_lexer import _command_flag_map, _command_function_map + assert _command_flag_map + assert _command_function_map \ No newline at end of file diff --git a/src/test/test_relation.py b/src/test/test_relation.py new file mode 100644 index 00000000..7460dadf --- /dev/null +++ b/src/test/test_relation.py @@ -0,0 +1,26 @@ +from umleditor.mvc_model import Relation + +def test_create_relation(): + rel = Relation("ent1", "ent2") + assert rel + +def test_get_source(): + rel = Relation("ent1", "ent2") + assert rel.get_source() == "ent1" + assert rel.get_source() != "ent2" + +def test_get_destination(): + rel = Relation("ent1", "ent2") + assert rel.get_destination() != "ent1" + assert rel.get_destination() == "ent2" + +def test_contains(): + rel = Relation("ent1", "ent2") + assert rel.contains("ent1") + assert rel.contains("ent2") + assert not rel.contains("ent3") + +def test_to_string(): + rel = Relation("ent1", "ent2") + assert str(rel) == "ent1 -> ent2" + assert str(rel) != "ent2 -> ent1" \ No newline at end of file diff --git a/src/test/test_uml_method.py b/src/test/test_uml_method.py new file mode 100644 index 00000000..26a80db9 --- /dev/null +++ b/src/test/test_uml_method.py @@ -0,0 +1,21 @@ +from umleditor.mvc_model import UML_Method + +def test_create_method(): + md1 = UML_Method("method1") + assert md1 + +def test_get_method_name(): + md1 = UML_Method("method1") + md2 = UML_Method("method2") + assert md1.get_method_name() == "method1" + assert md1.get_method_name() != "method2" + assert md2.get_method_name() != "method1" + assert md2.get_method_name() == "method2" + +def test_set_method_name(): + md1 = UML_Method("method1") + assert md1.get_method_name() == "method1" + assert md1.get_method_name() != "method2" + md1.set_method_name("method2") + assert md1.get_method_name() != "method1" + assert md1.get_method_name() == "method2" \ No newline at end of file From 4cc1833a0a8a1e599ceb5a0a76f6fdeb1a4517eb Mon Sep 17 00:00:00 2001 From: Peter F Date: Thu, 22 Feb 2024 20:49:49 -0500 Subject: [PATCH 026/144] Refactors - Controller parent class that contains all shared functionality - Parse works for everything again (finally) - there's nothing suspicious going on in parse - all imports are done correctly now --- main.py | 9 +++--- src/umleditor/mvc_controller/__init__.py | 1 + .../mvc_controller/cli_controller.py | 14 ++++++++ src/umleditor/mvc_controller/controller.py | 32 +++++++------------ src/umleditor/mvc_view/__init__.py | 1 - 5 files changed, 31 insertions(+), 26 deletions(-) create mode 100644 src/umleditor/mvc_controller/cli_controller.py diff --git a/main.py b/main.py index 3b77d37e..9cb45834 100644 --- a/main.py +++ b/main.py @@ -1,14 +1,13 @@ -# from Controller import Controller -# from umleditor.mvc_controller.controller import Controller from umleditor.mvc_controller import Controller +from umleditor.mvc_controller.cli_controller import CLI_Controller def debug_main(): - app = Controller() + app = CLI_Controller() app.run() def main(): try: - app = Controller() + app = CLI_Controller() app.run() except KeyboardInterrupt: # This handles ctrl+C @@ -18,7 +17,7 @@ def main(): pass except Exception as e: # Never expect errors to be caught here - print('Oh no! Unexpected Error!') + print(e) if __name__ == '__main__': if not __debug__: diff --git a/src/umleditor/mvc_controller/__init__.py b/src/umleditor/mvc_controller/__init__.py index 738cfb38..77671e46 100644 --- a/src/umleditor/mvc_controller/__init__.py +++ b/src/umleditor/mvc_controller/__init__.py @@ -1,4 +1,5 @@ from umleditor.mvc_controller.controller import Controller +from umleditor.mvc_controller.cli_controller import CLI_Controller # We probably won't need these, but leaving for quick include later. # from controller_input import read_file, read_line # from controller_output import write, write_file diff --git a/src/umleditor/mvc_controller/cli_controller.py b/src/umleditor/mvc_controller/cli_controller.py new file mode 100644 index 00000000..9a38a513 --- /dev/null +++ b/src/umleditor/mvc_controller/cli_controller.py @@ -0,0 +1,14 @@ +from umleditor.mvc_controller.controller import Controller +import umleditor.mvc_controller.controller_output as controller_output +from umleditor.mvc_controller.controller_input import read_line +from umleditor.mvc_model.diagram import Diagram + +class CLI_Controller (Controller): + def __init__(self): + super().__init__() + + def run(self): + while self._should_quit == False: + out = super().run(read_line()) + if out != None: + controller_output.write(out) \ No newline at end of file diff --git a/src/umleditor/mvc_controller/controller.py b/src/umleditor/mvc_controller/controller.py index e4c48d0d..66c1537d 100644 --- a/src/umleditor/mvc_controller/controller.py +++ b/src/umleditor/mvc_controller/controller.py @@ -1,45 +1,37 @@ -from .controller_input import read_file, read_line +from .controller_input import read_line import umleditor.mvc_controller.controller_output as controller_output -from umleditor.mvc_controller.uml_parser import parse from .serializer import CustomJSONEncoder, serialize, deserialize +from umleditor.mvc_controller.uml_parser import parse from umleditor.mvc_model import CustomExceptions as CE from umleditor.mvc_model.diagram import Diagram -from umleditor.mvc_model import help_menu from umleditor.mvc_controller.uml_parser import check_args import os class Controller: - def __init__(self) -> None: - self._should_quit = False - self._diagram = Diagram() + def __init__(self, d:Diagram = Diagram(), q:bool = False) -> None: + self._should_quit = q + self._diagram = d - - def run(self) -> None: - while not self._should_quit: - s = read_line() + def run(self, line:str) -> str: try: #parse the command - input = parse(self, s) + input = parse(self, line) #return from input is [function object, arg1,...,argn] command = input[0] args = input[1:] #execute the command - out = command(*args) - - #write output if it was something - if out != None: - controller_output.write(out) + return command(*args) except TypeError as t: - controller_output.write(CE.InvalidArgCountError(t)) + return str(CE.InvalidArgCountError(t)) except ValueError as v: - controller_output.write(CE.NeedsMoreInput()) + return str(CE.NeedsMoreInput()) except Exception as e: - controller_output.write(str(e)) - + return str(e) + def quit(self): '''Basic Quit Routine. Prompts user to save, where to save, validates input. diff --git a/src/umleditor/mvc_view/__init__.py b/src/umleditor/mvc_view/__init__.py index 7f27a2f7..e69de29b 100644 --- a/src/umleditor/mvc_view/__init__.py +++ b/src/umleditor/mvc_view/__init__.py @@ -1 +0,0 @@ -from .cli_lexer import lex_input as lex \ No newline at end of file From c4837ce09765eb10d1459e4967946eb597cd01f3 Mon Sep 17 00:00:00 2001 From: AdamG-L Date: Thu, 22 Feb 2024 21:23:09 -0500 Subject: [PATCH 027/144] Adding multi lists within class card --- main.py | 4 +-- .../mvc_controller/controller_GUI.py | 31 ++++++++++++------- src/umleditor/mvc_view/gui_view/class_card.py | 30 ++++++++++++------ src/umleditor/mvc_view/gui_view/uml.ui | 16 +++++----- src/umleditor/mvc_view/gui_view/view_GUI.py | 4 ++- 5 files changed, 52 insertions(+), 33 deletions(-) diff --git a/main.py b/main.py index 184f7690..221c9cfa 100644 --- a/main.py +++ b/main.py @@ -46,5 +46,5 @@ def mainGUI(): if not __debug__: debug_main() else: - main() - #mainGUI() + #main() + mainGUI() diff --git a/src/umleditor/mvc_controller/controller_GUI.py b/src/umleditor/mvc_controller/controller_GUI.py index 7b99f1cb..0363e1a9 100644 --- a/src/umleditor/mvc_controller/controller_GUI.py +++ b/src/umleditor/mvc_controller/controller_GUI.py @@ -1,5 +1,6 @@ from umleditor.mvc_view.gui_view.view_GUI import ViewGUI from umleditor.mvc_model.diagram import Diagram +from umleditor.mvc_controller.uml_parser import parse from PyQt6 import QtWidgets from PyQt6.QtWidgets import QInputDialog, QLineEdit from PyQt6.QtCore import QDir @@ -14,20 +15,28 @@ def __init__(self, window: ViewGUI) -> None: self._window.get_signal().connect(self.run) def run(self, task: str): + try: + #parse the command + parsedTask = parse(self, task) + #return from input is [function object, arg1,...,argn] + command = parsedTask[0] + args = parsedTask[1:] + #execute the command + out = command(*args) + except Exception as e: + self._window.invalid_input_message(str(e)) + return + # Successful task if "class -a" in task: - self.run_add_class(task) + self.add_class(task) + # Parse / Run Command # If success, return true # Else, create error messagebox and return false - def run_add_class(self, task): - #TODO Parse - success = False - warning = "Invalid class name!" - print(task) - if success: - self._window.close_class_dialog() - self._window.add_class_card() - else: - self._window.invalid_input_message(warning) + def add_class(self, task): + self._window.close_class_dialog() + self._window.add_class_card() + #else: + # self._window.invalid_input_message(warning) diff --git a/src/umleditor/mvc_view/gui_view/class_card.py b/src/umleditor/mvc_view/gui_view/class_card.py index 598110b0..e8080e6c 100644 --- a/src/umleditor/mvc_view/gui_view/class_card.py +++ b/src/umleditor/mvc_view/gui_view/class_card.py @@ -8,20 +8,29 @@ def __init__(self): self.initUI() def initUI(self): - self.layout = QVBoxLayout() + layout = QVBoxLayout() # Create list widget - self.list_widget = QListWidget() - self.list_widget.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) - self.list_widget.customContextMenuRequested.connect(self.showContextMenu) - self.layout.addWidget(self.list_widget) + self.list_field = QListWidget() + self.list_method = QListWidget() + self.list_relation = QListWidget() + self.list_field.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + self.list_field.customContextMenuRequested.connect(self.show_field_menu) + #self.list_widget.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + #self.list_widget.customContextMenuRequested.connect(self.show_context_menu) + layout.addWidget(self.list_field) + layout.addWidget(self.list_method) + layout.addWidget(self.list_relation) - self.setLayout(self.layout) + self.setLayout(layout) + self.setFixedSize(100,200) # Add some initial items to the list - for i in range(5): - self.list_widget.addItem(f"Item {i}") - + #for i in range(5): + # self.list_widget.addItem(f"Item {i}") + def show_field_menu(self, position): + print("Field List selected") +''' def showContextMenu(self, position): item = self.list_widget.itemAt(position) if item is not None: @@ -33,4 +42,5 @@ def showContextMenu(self, position): def editItem(self, item): index = self.list_widget.row(item) - print(item.text()) \ No newline at end of file + print(item.text()) +''' \ No newline at end of file diff --git a/src/umleditor/mvc_view/gui_view/uml.ui b/src/umleditor/mvc_view/gui_view/uml.ui index a5b66fc4..d9794d3e 100644 --- a/src/umleditor/mvc_view/gui_view/uml.ui +++ b/src/umleditor/mvc_view/gui_view/uml.ui @@ -11,7 +11,7 @@ - MainWindow + UML Editor @@ -41,6 +41,7 @@ + @@ -48,16 +49,8 @@ - - - View - - - - - @@ -85,6 +78,11 @@ Help + + + Exit + + diff --git a/src/umleditor/mvc_view/gui_view/view_GUI.py b/src/umleditor/mvc_view/gui_view/view_GUI.py index 649c0b0d..1abf737a 100644 --- a/src/umleditor/mvc_view/gui_view/view_GUI.py +++ b/src/umleditor/mvc_view/gui_view/view_GUI.py @@ -15,6 +15,7 @@ def __init__(self, *args, **kwargs): print(os.path.dirname(__file__)) self._ui = uic.loadUi(os.path.join(os.path.dirname(__file__),"uml.ui"), self) self.connect_menu() + self._x = 0 def get_signal(self): return self._process_task_signal @@ -42,4 +43,5 @@ def close_class_dialog(self): def add_class_card(self): self._class_card = ClassCard() # Add the custom widget to the central widget of the main window - self._ui.gridLayout.addWidget(self._class_card, 0, 0) \ No newline at end of file + self._ui.gridLayout.addWidget(self._class_card, self._x, self._x) + self._x = self._x + 1 \ No newline at end of file From 024aa8865f61933033cc1dc53d758f07904a097b Mon Sep 17 00:00:00 2001 From: AdamG-L Date: Thu, 22 Feb 2024 21:25:46 -0500 Subject: [PATCH 028/144] Commented out gui start --- main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index 221c9cfa..184f7690 100644 --- a/main.py +++ b/main.py @@ -46,5 +46,5 @@ def mainGUI(): if not __debug__: debug_main() else: - #main() - mainGUI() + main() + #mainGUI() From 6a99b00990044cfc3d36514021829d0e1db4fe94 Mon Sep 17 00:00:00 2001 From: Peter F Date: Thu, 22 Feb 2024 21:56:12 -0500 Subject: [PATCH 029/144] Name Changes and GUI Controller Update - GUI controller now inherits from controller - GUI controller named better - Minor CLI Syntax updates - main now imports according to renamed files --- main.py | 2 +- src/umleditor/mvc_controller/cli_controller.py | 9 ++++++--- src/umleditor/mvc_controller/controller.py | 8 ++++---- .../{controller_GUI.py => gui_controller.py} | 14 ++++---------- 4 files changed, 15 insertions(+), 18 deletions(-) rename src/umleditor/mvc_controller/{controller_GUI.py => gui_controller.py} (73%) diff --git a/main.py b/main.py index 6778b2be..403e2f4e 100644 --- a/main.py +++ b/main.py @@ -2,7 +2,7 @@ from umleditor.mvc_controller.cli_controller import CLI_Controller from umleditor.mvc_view.gui_view.view_GUI import ViewGUI -from umleditor.mvc_controller.controller_GUI import ControllerGUI +from umleditor.mvc_controller.gui_controller import ControllerGUI import sys from PyQt6 import QtWidgets diff --git a/src/umleditor/mvc_controller/cli_controller.py b/src/umleditor/mvc_controller/cli_controller.py index 9a38a513..7eef804a 100644 --- a/src/umleditor/mvc_controller/cli_controller.py +++ b/src/umleditor/mvc_controller/cli_controller.py @@ -9,6 +9,9 @@ def __init__(self): def run(self): while self._should_quit == False: - out = super().run(read_line()) - if out != None: - controller_output.write(out) \ No newline at end of file + try: + out = super().run(read_line()) + if out != None: + controller_output.write(out) + except Exception as e: + controller_output.write(e) \ No newline at end of file diff --git a/src/umleditor/mvc_controller/controller.py b/src/umleditor/mvc_controller/controller.py index 66c1537d..e329b832 100644 --- a/src/umleditor/mvc_controller/controller.py +++ b/src/umleditor/mvc_controller/controller.py @@ -26,11 +26,11 @@ def run(self, line:str) -> str: return command(*args) except TypeError as t: - return str(CE.InvalidArgCountError(t)) - except ValueError as v: - return str(CE.NeedsMoreInput()) + raise CE.InvalidArgCountError(t) + except ValueError: + raise CE.NeedsMoreInput() except Exception as e: - return str(e) + raise e def quit(self): '''Basic Quit Routine. Prompts user to save, where to save, diff --git a/src/umleditor/mvc_controller/controller_GUI.py b/src/umleditor/mvc_controller/gui_controller.py similarity index 73% rename from src/umleditor/mvc_controller/controller_GUI.py rename to src/umleditor/mvc_controller/gui_controller.py index 0363e1a9..b0b2bae7 100644 --- a/src/umleditor/mvc_controller/controller_GUI.py +++ b/src/umleditor/mvc_controller/gui_controller.py @@ -1,14 +1,14 @@ from umleditor.mvc_view.gui_view.view_GUI import ViewGUI from umleditor.mvc_model.diagram import Diagram -from umleditor.mvc_controller.uml_parser import parse from PyQt6 import QtWidgets from PyQt6.QtWidgets import QInputDialog, QLineEdit from PyQt6.QtCore import QDir +from umleditor.mvc_controller.controller import Controller - -class ControllerGUI: +class ControllerGUI (Controller): def __init__(self, window: ViewGUI) -> None: + super().__init__() self._window = window self._diagram = Diagram() # Used for detecting when tasks need run @@ -16,13 +16,7 @@ def __init__(self, window: ViewGUI) -> None: def run(self, task: str): try: - #parse the command - parsedTask = parse(self, task) - #return from input is [function object, arg1,...,argn] - command = parsedTask[0] - args = parsedTask[1:] - #execute the command - out = command(*args) + out = super().run(task) except Exception as e: self._window.invalid_input_message(str(e)) return From e7b18e2ba24cadec3516e49f6965d031083da7e4 Mon Sep 17 00:00:00 2001 From: almostTaklu Date: Thu, 22 Feb 2024 22:12:49 -0500 Subject: [PATCH 030/144] Finished relationType --- src/umleditor/mvc_model/custom_exceptions.py | 15 ++++++++++++++- src/umleditor/mvc_model/diagram.py | 4 ++-- src/umleditor/mvc_model/help_command.py | 3 ++- src/umleditor/mvc_model/relation.py | 3 +++ 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/umleditor/mvc_model/custom_exceptions.py b/src/umleditor/mvc_model/custom_exceptions.py index 56d66ae5..9e405a99 100644 --- a/src/umleditor/mvc_model/custom_exceptions.py +++ b/src/umleditor/mvc_model/custom_exceptions.py @@ -87,7 +87,20 @@ class InvalidRelationTypeError(Error): """ def __init__(self, source, destination): - super().__init__(f"Relation between '{source} -> {destination}' has no types.") + super().__init__(f"Relation between '{source} -> {destination}' has invalid type.") + + class InvalidRelationNewTypeError(Error): + """ + Exception raised when the relation being added has invalid new type. + + Args: + source (Entity): The source of the relation that was being added. + destination (Entity): The destination of the relation that was + being added. + + """ + def __init__(self, source, destination): + super().__init__(f"Relation between '{source} -> {destination}' has invalid new type.") #===============================================================================# #Parser/Controller Exceptions diff --git a/src/umleditor/mvc_model/diagram.py b/src/umleditor/mvc_model/diagram.py index 5607e40d..0362c382 100644 --- a/src/umleditor/mvc_model/diagram.py +++ b/src/umleditor/mvc_model/diagram.py @@ -185,7 +185,7 @@ def add_relation(self,source, destination, type): """ # Check for valid relationship type if type not in Relation.RELATIONSHIP_TYPE: - raise CustomExceptions.InvalidRelationTypeError(type) + raise CustomExceptions.InvalidRelationTypeError(source, destination) # Check for valid source and destination if source not in self._entities: @@ -247,7 +247,7 @@ def change_relation_type(self, source, destination, new_type): """ # Check for valid relationship type if new_type not in Relation.RELATIONSHIP_TYPE: - raise CustomExceptions.InvalidRelationTypeError(new_type) + raise CustomExceptions.InvalidRelationNewTypeError(source, destination) for rel in self._relations: if rel.get_source() == self._entities[source] and rel.get_destination() == self._entities[destination]: diff --git a/src/umleditor/mvc_model/help_command.py b/src/umleditor/mvc_model/help_command.py index babcbfdc..c380e3f7 100644 --- a/src/umleditor/mvc_model/help_command.py +++ b/src/umleditor/mvc_model/help_command.py @@ -31,7 +31,8 @@ def help_menu(): "att -r class 'old' 'new' - renames an attribute from name 'old' to name 'new' in class 'class'\n" #Relation Commands "Relation Commands:\n\t" - "rel -a 'src' 'dest' - adds a relationship between class 'src' and class 'dest' assuming both are valid\n\t" + "rel -a 'src' 'dest' 'type' - adds a relationship between class 'src' and class 'dest' of type 'type'\n\t" + "rel -t 'src' 'dest' 'type' - changes the type of the relationship between class 'src' and class 'dest' to 'new type'\n\t" "rel -d 'src' 'dest' - deletes a relationship between class 'src' and class 'dest' if one exists\n" #List Commands "List Flags: \n\t" diff --git a/src/umleditor/mvc_model/relation.py b/src/umleditor/mvc_model/relation.py index 2e5899be..fcf14be6 100644 --- a/src/umleditor/mvc_model/relation.py +++ b/src/umleditor/mvc_model/relation.py @@ -17,6 +17,9 @@ def __init__(self, type, source=Entity(), destination=Entity()): Returns: None. """ + if type not in self.RELATIONSHIP_TYPE: + raise ValueError(f"Invalid relationship type: {type}") + self._source = source self._destination = destination self._type = type From 228faf19e1d8322cbb6051c250ed5da363b4cae7 Mon Sep 17 00:00:00 2001 From: almostTaklu Date: Thu, 22 Feb 2024 22:19:28 -0500 Subject: [PATCH 031/144] Update CLI lexer command and function maps --- src/umleditor/mvc_view/cli_lexer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/umleditor/mvc_view/cli_lexer.py b/src/umleditor/mvc_view/cli_lexer.py index 79c63d4b..a1f6adbd 100644 --- a/src/umleditor/mvc_view/cli_lexer.py +++ b/src/umleditor/mvc_view/cli_lexer.py @@ -6,7 +6,7 @@ "class" : ["a","d","r"], "list" : ["a","c","r","d"], "att" : ["a","d","r"], - "rel" : ["a","d", "t"], + "rel" : ["a","t", "d"], "save" : [""], "load" : [""], "exit" : [""], @@ -20,7 +20,7 @@ "class" : ["add_entity","delete_entity","rename_entity"], "list" : ["list_everything","list_entities","list_relations","list_entity_details"], "att" : ["add_attribute","delete_attribute","rename_attribute"], - "rel" : ["add_relation","delete_relation", "change_relation_type"], + "rel" : ["add_relation", "change_relation_type", "delete_relation"], "save" : ["save"], "load" : ["load"], "exit" : ["quit"], From a3f851e08b788970a7e99631f6208489d2e29ad3 Mon Sep 17 00:00:00 2001 From: Peter Date: Fri, 23 Feb 2024 14:10:29 -0500 Subject: [PATCH 032/144] Fixed test_imports, added test_help --- src/test/test_help.py | 10 ++++++++++ src/test/test_imports.py | 6 +++--- 2 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 src/test/test_help.py diff --git a/src/test/test_help.py b/src/test/test_help.py new file mode 100644 index 00000000..a071a171 --- /dev/null +++ b/src/test/test_help.py @@ -0,0 +1,10 @@ +from umleditor.mvc_controller.uml_lexer import _command_flag_map +from umleditor.mvc_model.help_command import help_menu +import re + +def test_help(): + menu = help_menu() + for key in _command_flag_map: + for flag in _command_flag_map[key]: + val = key + " -" + flag + assert re.search(val, menu) != None, (val + " not in help menu") \ No newline at end of file diff --git a/src/test/test_imports.py b/src/test/test_imports.py index d74b7ca9..66e5e3aa 100644 --- a/src/test/test_imports.py +++ b/src/test/test_imports.py @@ -35,8 +35,8 @@ def test_serialzer_imports(): assert deserialize def test_import_parser(): - from umleditor.mvc_controller.uml_parser import Parser - assert Parser + from umleditor.mvc_controller.uml_parser import parse + assert parse def test_custom_exceptions_import(): from umleditor.mvc_model.custom_exceptions import CustomExceptions @@ -47,6 +47,6 @@ def test_help_import(): assert help_menu def test_cli_lexer_import(): - from umleditor.mvc_view.cli_lexer import _command_flag_map, _command_function_map + from umleditor.mvc_controller.uml_lexer import _command_flag_map, _command_function_map assert _command_flag_map assert _command_function_map \ No newline at end of file From 55d739c5563faaa6b39efc6e2a0ca5c6ffe9d693 Mon Sep 17 00:00:00 2001 From: AdamG-L Date: Fri, 23 Feb 2024 16:21:42 -0500 Subject: [PATCH 033/144] Fixing style of cards --- main.py | 4 +- .../mvc_controller/gui_controller.py | 5 +- src/umleditor/mvc_view/gui_view/class_card.py | 52 +++++++++++++------ src/umleditor/mvc_view/gui_view/view_GUI.py | 4 +- 4 files changed, 41 insertions(+), 24 deletions(-) diff --git a/main.py b/main.py index 403e2f4e..cf10f68e 100644 --- a/main.py +++ b/main.py @@ -49,5 +49,5 @@ def mainGUI(): if not __debug__: debug_main() else: - main() - #mainGUI() + #main() + mainGUI() diff --git a/src/umleditor/mvc_controller/gui_controller.py b/src/umleditor/mvc_controller/gui_controller.py index b0b2bae7..8e5a7bd3 100644 --- a/src/umleditor/mvc_controller/gui_controller.py +++ b/src/umleditor/mvc_controller/gui_controller.py @@ -29,8 +29,7 @@ def run(self, task: str): # Else, create error messagebox and return false def add_class(self, task): + entity_name = task.split()[-1] self._window.close_class_dialog() - self._window.add_class_card() - #else: - # self._window.invalid_input_message(warning) + self._window.add_class_card(entity_name) diff --git a/src/umleditor/mvc_view/gui_view/class_card.py b/src/umleditor/mvc_view/gui_view/class_card.py index e8080e6c..868f29a9 100644 --- a/src/umleditor/mvc_view/gui_view/class_card.py +++ b/src/umleditor/mvc_view/gui_view/class_card.py @@ -1,34 +1,52 @@ -from PyQt6.QtWidgets import QWidget, QVBoxLayout, QListWidget, QMenu, QLineEdit +from PyQt6.QtWidgets import QWidget, QVBoxLayout, QListWidget, QMenu, QLineEdit, QLabel from PyQt6.QtGui import QAction from PyQt6.QtCore import Qt class ClassCard(QWidget): - def __init__(self): + def __init__(self, name: str): super().__init__() + self.set_name(name) self.initUI() + + def set_name(self, name: str): + self._name = name def initUI(self): layout = QVBoxLayout() - # Create list widget - self.list_field = QListWidget() - self.list_method = QListWidget() - self.list_relation = QListWidget() - self.list_field.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) - self.list_field.customContextMenuRequested.connect(self.show_field_menu) - #self.list_widget.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) - #self.list_widget.customContextMenuRequested.connect(self.show_context_menu) - layout.addWidget(self.list_field) - layout.addWidget(self.list_method) - layout.addWidget(self.list_relation) + # Class label + self._class_label = QLabel(self._name) + self._class_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + # Create list widgets + self._list_field = QListWidget() + self._list_method = QListWidget() + self._list_relation = QListWidget() + # Connect right click + self._list_field.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + self._list_field.customContextMenuRequested.connect(self.show_field_menu) + # Add Widgets to class card + layout.addWidget(self._class_label) + layout.addWidget(self._list_field) + layout.addWidget(self._list_method) + layout.addWidget(self._list_relation) + + # Set border style for list widgets + self._list_field.setStyleSheet("border: 1px solid black;") + self._list_method.setStyleSheet("border: 1px solid black; border-bottom: none; border-top: none;") + self._list_relation.setStyleSheet("border: 1px solid black;") + + # Set style for class label + self._class_label.setStyleSheet("background-color: powderblue;") + self._class_label.setMinimumHeight(30) + + layout.setSpacing(0) self.setLayout(layout) + self.setStyleSheet("background-color: white;") + self.setFixedSize(150,200) - self.setFixedSize(100,200) - # Add some initial items to the list - #for i in range(5): - # self.list_widget.addItem(f"Item {i}") def show_field_menu(self, position): + print("Field List selected") ''' def showContextMenu(self, position): diff --git a/src/umleditor/mvc_view/gui_view/view_GUI.py b/src/umleditor/mvc_view/gui_view/view_GUI.py index 1abf737a..05b48bda 100644 --- a/src/umleditor/mvc_view/gui_view/view_GUI.py +++ b/src/umleditor/mvc_view/gui_view/view_GUI.py @@ -40,8 +40,8 @@ def confirm_class_clicked(self): def close_class_dialog(self): self._dialog.reject() - def add_class_card(self): - self._class_card = ClassCard() + def add_class_card(self, name: str): + self._class_card = ClassCard(name) # Add the custom widget to the central widget of the main window self._ui.gridLayout.addWidget(self._class_card, self._x, self._x) self._x = self._x + 1 \ No newline at end of file From 5bfd505f606ea9f3b60be301fb32b6a43a98fbcb Mon Sep 17 00:00:00 2001 From: AdamG-L Date: Fri, 23 Feb 2024 17:04:05 -0500 Subject: [PATCH 034/144] Class Card add fields funct. --- src/umleditor/mvc_view/gui_view/class_card.py | 60 +++++++++++++++---- 1 file changed, 49 insertions(+), 11 deletions(-) diff --git a/src/umleditor/mvc_view/gui_view/class_card.py b/src/umleditor/mvc_view/gui_view/class_card.py index 868f29a9..dc4dedf4 100644 --- a/src/umleditor/mvc_view/gui_view/class_card.py +++ b/src/umleditor/mvc_view/gui_view/class_card.py @@ -1,4 +1,4 @@ -from PyQt6.QtWidgets import QWidget, QVBoxLayout, QListWidget, QMenu, QLineEdit, QLabel +from PyQt6.QtWidgets import QWidget, QVBoxLayout, QListWidget, QMenu, QLineEdit, QLabel, QListWidgetItem from PyQt6.QtGui import QAction from PyQt6.QtCore import Qt @@ -12,18 +12,20 @@ def set_name(self, name: str): self._name = name def initUI(self): + # Container for all of our individual widgets layout = QVBoxLayout() # Class label self._class_label = QLabel(self._name) self._class_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + # Create list widgets self._list_field = QListWidget() self._list_method = QListWidget() self._list_relation = QListWidget() + # Connect right click - self._list_field.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) - self._list_field.customContextMenuRequested.connect(self.show_field_menu) + self.connect_menus() # Add Widgets to class card layout.addWidget(self._class_label) @@ -31,23 +33,59 @@ def initUI(self): layout.addWidget(self._list_method) layout.addWidget(self._list_relation) + #Set styles + self.set_styles() + + # Size and spacing + layout.setSpacing(0) + self.setFixedSize(150,200) + + self.setLayout(layout) + + def connect_menus(self): + # Connect class label + self._class_label.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + self._class_label.customContextMenuRequested.connect(self.show_class_menu) + # Connect field list + self._list_field.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + self._list_field.customContextMenuRequested.connect(self.show_field_menu) + + def set_styles(self): # Set border style for list widgets - self._list_field.setStyleSheet("border: 1px solid black;") + self._list_field.setStyleSheet("border: 1px solid black; border-top: none") self._list_method.setStyleSheet("border: 1px solid black; border-bottom: none; border-top: none;") self._list_relation.setStyleSheet("border: 1px solid black;") - # Set style for class label - self._class_label.setStyleSheet("background-color: powderblue;") + self._class_label.setStyleSheet("background-color: powderblue; border: 1px solid black;") self._class_label.setMinimumHeight(30) - - layout.setSpacing(0) - self.setLayout(layout) + # Set style for entire widget self.setStyleSheet("background-color: white;") - self.setFixedSize(150,200) - def show_field_menu(self, position): + def show_class_menu(self, position): + print("Class menu selected") + menu = QMenu() + field_action = QAction("Add Field", self) + field_action.triggered.connect(self.add_field_clicked) + menu.addAction(field_action) + menu.exec(self._class_label.mapToGlobal(position)) + def show_field_menu(self, position): print("Field List selected") + + def add_field_clicked(self): + # Create new field + item = QListWidgetItem() + self._list_field.addItem(item) + field_text = QLineEdit() + field_text.returnPressed.connect(self.verify_input) + self._list_field.setItemWidget(item, field_text) + field_text.setPlaceholderText("Enter Field Here") + field_text.setFocus() + print("Add field action clicked") + + def verify_input(self): + print("Return key pressed") + ''' def showContextMenu(self, position): item = self.list_widget.itemAt(position) From 07f5c4bdc54d82427e0b759abc24f6ef210bb878 Mon Sep 17 00:00:00 2001 From: AdamG-L Date: Fri, 23 Feb 2024 18:25:12 -0500 Subject: [PATCH 035/144] Passing a widget through the signal --- src/umleditor/mvc_controller/gui_controller.py | 10 ++++++---- src/umleditor/mvc_view/gui_view/class_card.py | 11 ++++++++--- src/umleditor/mvc_view/gui_view/view_GUI.py | 7 ++----- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/umleditor/mvc_controller/gui_controller.py b/src/umleditor/mvc_controller/gui_controller.py index 8e5a7bd3..6767e1c7 100644 --- a/src/umleditor/mvc_controller/gui_controller.py +++ b/src/umleditor/mvc_controller/gui_controller.py @@ -4,6 +4,7 @@ from PyQt6.QtWidgets import QInputDialog, QLineEdit from PyQt6.QtCore import QDir from umleditor.mvc_controller.controller import Controller +from umleditor.mvc_view.gui_view.class_input_dialog import ClassInputDialog class ControllerGUI (Controller): @@ -14,7 +15,7 @@ def __init__(self, window: ViewGUI) -> None: # Used for detecting when tasks need run self._window.get_signal().connect(self.run) - def run(self, task: str): + def run(self, task: str, widget: QtWidgets): try: out = super().run(task) except Exception as e: @@ -22,14 +23,15 @@ def run(self, task: str): return # Successful task if "class -a" in task: - self.add_class(task) + self.add_class(task, widget) # Parse / Run Command # If success, return true # Else, create error messagebox and return false - def add_class(self, task): + def add_class(self, task, widget): + # Close dialog box + widget.reject() entity_name = task.split()[-1] - self._window.close_class_dialog() self._window.add_class_card(entity_name) diff --git a/src/umleditor/mvc_view/gui_view/class_card.py b/src/umleditor/mvc_view/gui_view/class_card.py index dc4dedf4..0d84629f 100644 --- a/src/umleditor/mvc_view/gui_view/class_card.py +++ b/src/umleditor/mvc_view/gui_view/class_card.py @@ -1,8 +1,11 @@ from PyQt6.QtWidgets import QWidget, QVBoxLayout, QListWidget, QMenu, QLineEdit, QLabel, QListWidgetItem from PyQt6.QtGui import QAction -from PyQt6.QtCore import Qt +from PyQt6.QtCore import Qt, pyqtSignal + class ClassCard(QWidget): + # Signal triggered for task processing + _process_task_signal = pyqtSignal(str) def __init__(self, name: str): super().__init__() self.set_name(name) @@ -77,13 +80,15 @@ def add_field_clicked(self): item = QListWidgetItem() self._list_field.addItem(item) field_text = QLineEdit() - field_text.returnPressed.connect(self.verify_input) + # lambda ensures text is only evaluated on enter + field_text.returnPressed.connect(lambda: self.verify_input(field_text.text())) self._list_field.setItemWidget(item, field_text) field_text.setPlaceholderText("Enter Field Here") field_text.setFocus() print("Add field action clicked") - def verify_input(self): + def verify_input(self, input): + self._process_task_signal.emit(input) print("Return key pressed") ''' diff --git a/src/umleditor/mvc_view/gui_view/view_GUI.py b/src/umleditor/mvc_view/gui_view/view_GUI.py index 05b48bda..eb2a1af1 100644 --- a/src/umleditor/mvc_view/gui_view/view_GUI.py +++ b/src/umleditor/mvc_view/gui_view/view_GUI.py @@ -8,7 +8,7 @@ class ViewGUI(QtWidgets.QMainWindow): # Signal triggered for task processing - _process_task_signal = pyqtSignal(str) + _process_task_signal = pyqtSignal(str, QWidget) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -35,10 +35,7 @@ def add_class_click(self): def confirm_class_clicked(self): task = 'class -a ' + self._dialog.input_text.text() # Emit signal to controller to handle task - self._process_task_signal.emit(task) - - def close_class_dialog(self): - self._dialog.reject() + self._process_task_signal.emit(task, self._dialog) def add_class_card(self, name: str): self._class_card = ClassCard(name) From 9184427d111f1a447f582df4fc6c8f64338da6b3 Mon Sep 17 00:00:00 2001 From: AdamG-L Date: Fri, 23 Feb 2024 22:09:26 -0500 Subject: [PATCH 036/144] Add comments to gui_controller --- .../mvc_controller/gui_controller.py | 52 +++++++++++++--- src/umleditor/mvc_view/gui_view/class_card.py | 60 +++++++++++-------- .../mvc_view/gui_view/custom_line_edit.py | 19 ++++++ src/umleditor/mvc_view/gui_view/view_GUI.py | 23 +++++-- 4 files changed, 118 insertions(+), 36 deletions(-) create mode 100644 src/umleditor/mvc_view/gui_view/custom_line_edit.py diff --git a/src/umleditor/mvc_controller/gui_controller.py b/src/umleditor/mvc_controller/gui_controller.py index 6767e1c7..5472c84c 100644 --- a/src/umleditor/mvc_controller/gui_controller.py +++ b/src/umleditor/mvc_controller/gui_controller.py @@ -7,15 +7,36 @@ from umleditor.mvc_view.gui_view.class_input_dialog import ClassInputDialog class ControllerGUI (Controller): - + """ + ControllerGui - Runs tasks signaled by ViewGui + Run commands require updates to the gui e.g. methods found + in run() + + Parameters: + window (ViewGUI): The window instance of ViewGUI. + """ + def __init__(self, window: ViewGUI) -> None: + """ + Initializes Diagram, sets the view as a class variable, + Connects signal that runs tasks + + Parameters: + window (ViewGUI): The window instance of ViewGUI. + """ super().__init__() self._window = window self._diagram = Diagram() - # Used for detecting when tasks need run self._window.get_signal().connect(self.run) def run(self, task: str, widget: QtWidgets): + """ + Runs the specified task. + + Parameters: + task (str): The task to run. + widget (QtWidgets): Used to set particular widget to its completed state + """ try: out = super().run(task) except Exception as e: @@ -24,14 +45,29 @@ def run(self, task: str, widget: QtWidgets): # Successful task if "class -a" in task: self.add_class(task, widget) - - # Parse / Run Command - # If success, return true - # Else, create error messagebox and return false + elif "fld -a" in task: + self.add_field(widget) - def add_class(self, task, widget): - # Close dialog box + def add_class(self, task: str, widget: QtWidgets): + """ + Closes dialog and creates class card. + + Parameters: + task (str): Used for class name. + widget: ClassInputDialog. + """ widget.reject() entity_name = task.split()[-1] self._window.add_class_card(entity_name) + + def add_field(self, widget): + """ + Makes text read-only and returns diagram to original state + + Parameters: + widget: The widget instance. + """ + widget.get_selected_line().setReadOnly(True) + widget.get_selected_line().setStyleSheet("background-color: white;") + widget.enable_all_items() diff --git a/src/umleditor/mvc_view/gui_view/class_card.py b/src/umleditor/mvc_view/gui_view/class_card.py index 0d84629f..80e6ac9f 100644 --- a/src/umleditor/mvc_view/gui_view/class_card.py +++ b/src/umleditor/mvc_view/gui_view/class_card.py @@ -1,11 +1,13 @@ from PyQt6.QtWidgets import QWidget, QVBoxLayout, QListWidget, QMenu, QLineEdit, QLabel, QListWidgetItem from PyQt6.QtGui import QAction from PyQt6.QtCore import Qt, pyqtSignal - +from umleditor.mvc_view.gui_view.custom_line_edit import CustomLineEdit class ClassCard(QWidget): # Signal triggered for task processing - _process_task_signal = pyqtSignal(str) + _process_task_signal = pyqtSignal(str, QWidget) + _enable_widgets_signal = pyqtSignal(bool, QWidget) + def __init__(self, name: str): super().__init__() self.set_name(name) @@ -59,7 +61,7 @@ def set_styles(self): self._list_method.setStyleSheet("border: 1px solid black; border-bottom: none; border-top: none;") self._list_relation.setStyleSheet("border: 1px solid black;") # Set style for class label - self._class_label.setStyleSheet("background-color: powderblue; border: 1px solid black;") + self._class_label.setStyleSheet("background-color: #6495ED; border: 1px solid black;") self._class_label.setMinimumHeight(30) # Set style for entire widget self.setStyleSheet("background-color: white;") @@ -76,32 +78,42 @@ def show_field_menu(self, position): print("Field List selected") def add_field_clicked(self): + self._enable_widgets_signal.emit(False, self) # Create new field item = QListWidgetItem() self._list_field.addItem(item) field_text = QLineEdit() + self._selected_line = field_text + field_text.setStyleSheet("background-color: #ADD8E6;") # lambda ensures text is only evaluated on enter - field_text.returnPressed.connect(lambda: self.verify_input(field_text.text())) + field_text.returnPressed.connect(lambda: self.verify_input(field_text.text(), field_text)) self._list_field.setItemWidget(item, field_text) field_text.setPlaceholderText("Enter Field Here") + field_text.setAlignment(Qt.AlignmentFlag.AlignCenter) field_text.setFocus() - print("Add field action clicked") - - def verify_input(self, input): - self._process_task_signal.emit(input) - print("Return key pressed") - -''' - def showContextMenu(self, position): - item = self.list_widget.itemAt(position) - if item is not None: - menu = QMenu() - edit_action = QAction("Edit", self) - edit_action.triggered.connect(lambda: self.editItem(item)) - menu.addAction(edit_action) - menu.exec(self.list_widget.mapToGlobal(position)) - - def editItem(self, item): - index = self.list_widget.row(item) - print(item.text()) -''' \ No newline at end of file + + def disable_unselected_items(self): + self._class_label.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu) + self._list_field.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu) + self._list_method.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu) + self._list_relation.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu) + + def enable_all_items(self): + self._class_label.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + self._list_field.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + self._list_method.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + self._list_relation.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + + def verify_input(self, input: str, widget: QWidget): + task = "fld -a " + self._class_label.text() + " " + input + self._process_task_signal.emit(task, self) + + def get_selected_line(self): + return self._selected_line + + + def get_task_signal(self): + return self._process_task_signal + + def get_disable_signal(self): + return self._enable_widgets_signal \ No newline at end of file diff --git a/src/umleditor/mvc_view/gui_view/custom_line_edit.py b/src/umleditor/mvc_view/gui_view/custom_line_edit.py new file mode 100644 index 00000000..3f376e39 --- /dev/null +++ b/src/umleditor/mvc_view/gui_view/custom_line_edit.py @@ -0,0 +1,19 @@ +from PyQt6.QtWidgets import QLineEdit + +class CustomLineEdit(QLineEdit): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def focusOutEvent(self, event): + # Prevent the QLineEdit from losing focus + pass + + def mousePressEvent(self, event): + # Ensure the QLineEdit keeps the focus when clicked + self.setFocus() + super().mousePressEvent(event) + + def mouseReleaseEvent(self, event): + # Ensure the QLineEdit keeps the focus when released + self.setFocus() + super().mouseReleaseEvent(event) \ No newline at end of file diff --git a/src/umleditor/mvc_view/gui_view/view_GUI.py b/src/umleditor/mvc_view/gui_view/view_GUI.py index eb2a1af1..50eb808d 100644 --- a/src/umleditor/mvc_view/gui_view/view_GUI.py +++ b/src/umleditor/mvc_view/gui_view/view_GUI.py @@ -1,7 +1,7 @@ import sys, os from PyQt6 import QtCore, QtGui, QtWidgets from PyQt6 import uic -from PyQt6.QtWidgets import QMessageBox, QWidget, QVBoxLayout +from PyQt6.QtWidgets import QMessageBox, QWidget, QVBoxLayout, QMenuBar, QLineEdit from PyQt6.QtCore import pyqtSignal from umleditor.mvc_view.gui_view.class_input_dialog import ClassInputDialog from umleditor.mvc_view.gui_view.class_card import ClassCard @@ -25,6 +25,9 @@ def connect_menu(self): def invalid_input_message(self, warning: str): QMessageBox.critical(self, "Error", warning) + + def forward_signal(self, task: str, widget: QWidget): + self._process_task_signal.emit(task, widget) ############# Add Class Methods ############ def add_class_click(self): @@ -38,7 +41,19 @@ def confirm_class_clicked(self): self._process_task_signal.emit(task, self._dialog) def add_class_card(self, name: str): - self._class_card = ClassCard(name) + class_card = ClassCard(name) + class_card.get_task_signal().connect(self.forward_signal) + class_card.get_disable_signal().connect(self.toggle_widgets) # Add the custom widget to the central widget of the main window - self._ui.gridLayout.addWidget(self._class_card, self._x, self._x) - self._x = self._x + 1 \ No newline at end of file + self._ui.gridLayout.addWidget(class_card, self._x, self._x) + self._x = self._x + 1 + + def toggle_widgets(self, enabled: bool, active_widget: QWidget): + for child_widget in self.findChildren(QWidget): + if isinstance(child_widget, ClassCard) or isinstance(child_widget, QMenuBar): + if enabled or child_widget is active_widget: + child_widget.setEnabled(True) + else: + child_widget.setEnabled(False) + active_widget.disable_unselected_items() + From 538b218a49bd88057c5afc18c41e6120a87ac3e1 Mon Sep 17 00:00:00 2001 From: AdamG-L Date: Fri, 23 Feb 2024 22:19:32 -0500 Subject: [PATCH 037/144] Documented view_gui --- src/umleditor/mvc_view/gui_view/class_card.py | 1 - .../mvc_view/gui_view/custom_line_edit.py | 19 ------ src/umleditor/mvc_view/gui_view/view_GUI.py | 59 +++++++++++++++++-- 3 files changed, 54 insertions(+), 25 deletions(-) delete mode 100644 src/umleditor/mvc_view/gui_view/custom_line_edit.py diff --git a/src/umleditor/mvc_view/gui_view/class_card.py b/src/umleditor/mvc_view/gui_view/class_card.py index 80e6ac9f..9818975b 100644 --- a/src/umleditor/mvc_view/gui_view/class_card.py +++ b/src/umleditor/mvc_view/gui_view/class_card.py @@ -1,7 +1,6 @@ from PyQt6.QtWidgets import QWidget, QVBoxLayout, QListWidget, QMenu, QLineEdit, QLabel, QListWidgetItem from PyQt6.QtGui import QAction from PyQt6.QtCore import Qt, pyqtSignal -from umleditor.mvc_view.gui_view.custom_line_edit import CustomLineEdit class ClassCard(QWidget): # Signal triggered for task processing diff --git a/src/umleditor/mvc_view/gui_view/custom_line_edit.py b/src/umleditor/mvc_view/gui_view/custom_line_edit.py deleted file mode 100644 index 3f376e39..00000000 --- a/src/umleditor/mvc_view/gui_view/custom_line_edit.py +++ /dev/null @@ -1,19 +0,0 @@ -from PyQt6.QtWidgets import QLineEdit - -class CustomLineEdit(QLineEdit): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - def focusOutEvent(self, event): - # Prevent the QLineEdit from losing focus - pass - - def mousePressEvent(self, event): - # Ensure the QLineEdit keeps the focus when clicked - self.setFocus() - super().mousePressEvent(event) - - def mouseReleaseEvent(self, event): - # Ensure the QLineEdit keeps the focus when released - self.setFocus() - super().mouseReleaseEvent(event) \ No newline at end of file diff --git a/src/umleditor/mvc_view/gui_view/view_GUI.py b/src/umleditor/mvc_view/gui_view/view_GUI.py index 50eb808d..8278e419 100644 --- a/src/umleditor/mvc_view/gui_view/view_GUI.py +++ b/src/umleditor/mvc_view/gui_view/view_GUI.py @@ -1,4 +1,4 @@ -import sys, os +import os from PyQt6 import QtCore, QtGui, QtWidgets from PyQt6 import uic from PyQt6.QtWidgets import QMessageBox, QWidget, QVBoxLayout, QMenuBar, QLineEdit @@ -7,10 +7,18 @@ from umleditor.mvc_view.gui_view.class_card import ClassCard class ViewGUI(QtWidgets.QMainWindow): - # Signal triggered for task processing + """ + ViewGUI - Main application window for the UML diagram editor. + + Signals: + _process_task_signal (str, QWidget): Signal triggered for task processing. + """ _process_task_signal = pyqtSignal(str, QWidget) def __init__(self, *args, **kwargs): + """ + Initializes the ViewGUI instance and connect menu buttons + """ super().__init__(*args, **kwargs) print(os.path.dirname(__file__)) self._ui = uic.loadUi(os.path.join(os.path.dirname(__file__),"uml.ui"), self) @@ -18,37 +26,78 @@ def __init__(self, *args, **kwargs): self._x = 0 def get_signal(self): + """ + Gets the process task signal. + + Returns: + pyqtSignal: The process task signal. + """ return self._process_task_signal def connect_menu(self): + """ + Connects menu actions to corresponding methods. + """ self._ui.actionAdd_Class.triggered.connect(self.add_class_click) def invalid_input_message(self, warning: str): + """ + Displays an error message dialog. + + Args: + warning (str): The warning message. + """ QMessageBox.critical(self, "Error", warning) def forward_signal(self, task: str, widget: QWidget): + """ + Forwards a signal to process a task. + + Args: + task (str): The task to process. + widget (QWidget): The widget associated with the task. + """ self._process_task_signal.emit(task, widget) ############# Add Class Methods ############ def add_class_click(self): + """ + Opens a dialog for adding a class and connects confirm button + """ self._dialog = ClassInputDialog() self._dialog.ok_button.clicked.connect(self.confirm_class_clicked) self._dialog.exec() def confirm_class_clicked(self): + """ + On Confirm emits signal to process task + """ task = 'class -a ' + self._dialog.input_text.text() # Emit signal to controller to handle task self._process_task_signal.emit(task, self._dialog) def add_class_card(self, name: str): + """ + Adds a class card widget to the main window. + Connects signals for sending tasks and disabling other widgets + + Args: + name (str): The name of the class. + """ class_card = ClassCard(name) class_card.get_task_signal().connect(self.forward_signal) - class_card.get_disable_signal().connect(self.toggle_widgets) - # Add the custom widget to the central widget of the main window + class_card.get_disable_signal().connect(self.enable_widgets) self._ui.gridLayout.addWidget(class_card, self._x, self._x) self._x = self._x + 1 - def toggle_widgets(self, enabled: bool, active_widget: QWidget): + def enable_widgets(self, enabled: bool, active_widget: QWidget): + """ + Toggles unselected Widgets. False = Disabled, True = Enabled + + Args: + enabled (bool): Flag indicating whether widgets should be enabled. + active_widget (QWidget): The active ClassCard widget. + """ for child_widget in self.findChildren(QWidget): if isinstance(child_widget, ClassCard) or isinstance(child_widget, QMenuBar): if enabled or child_widget is active_widget: From 6bc416bee85eec4439942a28ce6b8988740ee8f6 Mon Sep 17 00:00:00 2001 From: AdamG-L Date: Fri, 23 Feb 2024 22:34:41 -0500 Subject: [PATCH 038/144] Documented class_card --- main.py | 4 +- .../mvc_controller/gui_controller.py | 1 + src/umleditor/mvc_view/gui_view/class_card.py | 86 +++++++++++++++++-- src/umleditor/mvc_view/gui_view/view_GUI.py | 5 +- 4 files changed, 86 insertions(+), 10 deletions(-) diff --git a/main.py b/main.py index cf10f68e..403e2f4e 100644 --- a/main.py +++ b/main.py @@ -49,5 +49,5 @@ def mainGUI(): if not __debug__: debug_main() else: - #main() - mainGUI() + main() + #mainGUI() diff --git a/src/umleditor/mvc_controller/gui_controller.py b/src/umleditor/mvc_controller/gui_controller.py index 5472c84c..8e25de74 100644 --- a/src/umleditor/mvc_controller/gui_controller.py +++ b/src/umleditor/mvc_controller/gui_controller.py @@ -70,4 +70,5 @@ def add_field(self, widget): widget.get_selected_line().setReadOnly(True) widget.get_selected_line().setStyleSheet("background-color: white;") widget.enable_all_items() + self._window.enable_widgets(True, self) diff --git a/src/umleditor/mvc_view/gui_view/class_card.py b/src/umleditor/mvc_view/gui_view/class_card.py index 9818975b..6531d5cc 100644 --- a/src/umleditor/mvc_view/gui_view/class_card.py +++ b/src/umleditor/mvc_view/gui_view/class_card.py @@ -3,19 +3,40 @@ from PyQt6.QtCore import Qt, pyqtSignal class ClassCard(QWidget): - # Signal triggered for task processing + """ + ClassCard - Widget representing a class in the UML diagram editor. + + Signals: + _process_task_signal (str, QWidget): Signal triggered for task processing. + _enable_widgets_signal (bool, QWidget): Signal triggered to enable/disable widget interactions. + """ _process_task_signal = pyqtSignal(str, QWidget) _enable_widgets_signal = pyqtSignal(bool, QWidget) def __init__(self, name: str): + """ + Initializes the ClassCard widget. + + Args: + name (str): The name of the class. + """ super().__init__() self.set_name(name) self.initUI() def set_name(self, name: str): + """ + Sets the name of the class. + + Args: + name (str): The name of the class. + """ self._name = name def initUI(self): + """ + Initializes the user interface of the ClassCard widget. + """ # Container for all of our individual widgets layout = QVBoxLayout() @@ -47,6 +68,9 @@ def initUI(self): self.setLayout(layout) def connect_menus(self): + """ + Connects right-click context menus for class label and lists. + """ # Connect class label self._class_label.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self._class_label.customContextMenuRequested.connect(self.show_class_menu) @@ -55,6 +79,9 @@ def connect_menus(self): self._list_field.customContextMenuRequested.connect(self.show_field_menu) def set_styles(self): + """ + Sets styles for the widgets. + """ # Set border style for list widgets self._list_field.setStyleSheet("border: 1px solid black; border-top: none") self._list_method.setStyleSheet("border: 1px solid black; border-bottom: none; border-top: none;") @@ -66,7 +93,12 @@ def set_styles(self): self.setStyleSheet("background-color: white;") def show_class_menu(self, position): - print("Class menu selected") + """ + Shows the context menu for the class label. + + Args: + position: The position of the context menu. + """ menu = QMenu() field_action = QAction("Add Field", self) field_action.triggered.connect(self.add_field_clicked) @@ -74,45 +106,87 @@ def show_class_menu(self, position): menu.exec(self._class_label.mapToGlobal(position)) def show_field_menu(self, position): - print("Field List selected") + """ + Shows the context menu for the field list. + + Args: + position: The position of the context menu. + """ + pass def add_field_clicked(self): + """ + Adds a field when the "Add Field" action is clicked. + """ + # Disables unselected interactions self._enable_widgets_signal.emit(False, self) - # Create new field + # Create field and add to list item = QListWidgetItem() self._list_field.addItem(item) field_text = QLineEdit() self._selected_line = field_text - field_text.setStyleSheet("background-color: #ADD8E6;") # lambda ensures text is only evaluated on enter field_text.returnPressed.connect(lambda: self.verify_input(field_text.text(), field_text)) + # Formatting / Style + field_text.setStyleSheet("background-color: #ADD8E6;") self._list_field.setItemWidget(item, field_text) field_text.setPlaceholderText("Enter Field Here") field_text.setAlignment(Qt.AlignmentFlag.AlignCenter) field_text.setFocus() def disable_unselected_items(self): + """ + Disables context menus for all items within the ClassCard + """ self._class_label.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu) self._list_field.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu) self._list_method.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu) self._list_relation.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu) def enable_all_items(self): + """ + Enables context menus for all items within the ClassCard + """ self._class_label.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self._list_field.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self._list_method.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self._list_relation.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) def verify_input(self, input: str, widget: QWidget): + """ + Sends a signal for the task to be processed. + + Args: + input (str): The input text. + widget (QWidget): The associated ClassCard widget. + """ task = "fld -a " + self._class_label.text() + " " + input self._process_task_signal.emit(task, self) def get_selected_line(self): + """ + Return the currently selected line. + + Returns: + QLineEdit: The currently selected line. + """ return self._selected_line def get_task_signal(self): + """ + Return the task signal to be connected. + + Returns: + pyqtSignal: The signal for task processing. + """ return self._process_task_signal - def get_disable_signal(self): + def get_enable_signal(self): + """ + Return the enable signal to be connected. + + Returns: + pyqtSignal: The signal for widget enabling. + """ return self._enable_widgets_signal \ No newline at end of file diff --git a/src/umleditor/mvc_view/gui_view/view_GUI.py b/src/umleditor/mvc_view/gui_view/view_GUI.py index 8278e419..5a065edd 100644 --- a/src/umleditor/mvc_view/gui_view/view_GUI.py +++ b/src/umleditor/mvc_view/gui_view/view_GUI.py @@ -86,7 +86,7 @@ def add_class_card(self, name: str): """ class_card = ClassCard(name) class_card.get_task_signal().connect(self.forward_signal) - class_card.get_disable_signal().connect(self.enable_widgets) + class_card.get_enable_signal().connect(self.enable_widgets) self._ui.gridLayout.addWidget(class_card, self._x, self._x) self._x = self._x + 1 @@ -104,5 +104,6 @@ def enable_widgets(self, enabled: bool, active_widget: QWidget): child_widget.setEnabled(True) else: child_widget.setEnabled(False) - active_widget.disable_unselected_items() + if isinstance(child_widget, ClassCard): + active_widget.disable_unselected_items() From 39db2032d72069c05eafd8606c1fc43762ecdc82 Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 24 Feb 2024 16:01:15 -0500 Subject: [PATCH 039/144] List functionality working based on current implementations. --- src/umleditor/mvc_model/diagram.py | 27 +++++++++++++++++---------- src/umleditor/mvc_model/entity.py | 10 +++++++--- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/umleditor/mvc_model/diagram.py b/src/umleditor/mvc_model/diagram.py index 7ec3845a..ff44baaf 100644 --- a/src/umleditor/mvc_model/diagram.py +++ b/src/umleditor/mvc_model/diagram.py @@ -120,28 +120,35 @@ def list_everything(self): def list_entity_details(self, entity_name): """ - Returns the attributes and relations of the entity. + Returns the fields, methods, params, and relations of the entity. Args: entity_name (str): The name of the entity to get details of. Raises: - None + CustomExceptions.EntityNotFoundError: If an entity with the old name + does not exist. Returns: - str: A templated string containing the attributes and relations. + str: A templated string containing the fields, methods, params + and relations of an entity. """ if not self._entities.__contains__(entity_name): raise CustomExceptions.EntityNotFoundError(entity_name) else: - entity = self._entities[entity_name] - att = entity._attributes - rels = [rel for rel in self._relations if rel.contains(entity)] - result ="\n" + entity_name + "'s Attributes:\n" - att_string = ', '.join(att) - result2 = entity_name + "'s Relations:\n" + ent = self._entities[entity_name] + fld = ent._fields + rels = [rel for rel in self._relations if rel.contains(ent)] + result = entity_name +":\n" + entity_name + "'s Fields:\n" + fld_string = ', '.join(fld) + result2 = entity_name + "'s Methods:\n" + mthd_string = "" + for m in ent._methods: + mthd_string += ', '.join(str(m) for m in ent._methods) + mthd_string += "\n" + result3 = entity_name + "'s Relations:\n" rel_string = ', '.join(str(rel) for rel in rels) - return result + att_string + "\n" + result2 + rel_string + return result + fld_string + "\n" + result2 + mthd_string + result3 + rel_string def list_entities(self): """ diff --git a/src/umleditor/mvc_model/entity.py b/src/umleditor/mvc_model/entity.py index 74001bf3..ac69eff1 100644 --- a/src/umleditor/mvc_model/entity.py +++ b/src/umleditor/mvc_model/entity.py @@ -285,7 +285,7 @@ def change_parameters(self, old_params: list[str], new_params: list[str]): def __str__(self): """ - Returns the name of the method. + Returns the name of the method and a list of it's parameters. Args: None. @@ -294,6 +294,10 @@ def __str__(self): None. Returns: - name (str): A string to represent the method. + name (str): A templated string to represent a method and + its list of parameters. """ - return self.get_method_name() \ No newline at end of file + result = self.get_method_name() + result += "\n\t" + self.get_method_name() + "'s Params:\n\t\t" + param_results = ', '.join(p for p in self._params) + return result + param_results \ No newline at end of file From fb85a7a26f7f61b7c030678969c75a2990814c37 Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 24 Feb 2024 16:15:49 -0500 Subject: [PATCH 040/144] Fixed two failing tests. Added additional tests for new files. --- src/test/test_imports.py | 40 +++++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/src/test/test_imports.py b/src/test/test_imports.py index d74b7ca9..6b5aab08 100644 --- a/src/test/test_imports.py +++ b/src/test/test_imports.py @@ -18,35 +18,53 @@ def test_import_method(): from umleditor.mvc_model import UML_Method assert UML_Method -def test_controller_input_imports(): +def test_import_controller_input(): from umleditor.mvc_controller.controller_input import read_line, read_file assert read_line assert read_file -def test_controller_output_imports(): +def test_import_controller_output(): from umleditor.mvc_controller.controller_output import write, write_file assert write assert write_file -def test_serialzer_imports(): +def test_import_serialzer(): from umleditor.mvc_controller.serializer import CustomJSONEncoder, serialize, deserialize assert CustomJSONEncoder assert serialize assert deserialize -def test_import_parser(): - from umleditor.mvc_controller.uml_parser import Parser - assert Parser +def test_import_uml_parser(): + from umleditor.mvc_controller.uml_parser import parse, check_args + assert parse + assert check_args -def test_custom_exceptions_import(): +def test_import_custom_exceptions(): from umleditor.mvc_model.custom_exceptions import CustomExceptions assert CustomExceptions -def test_help_import(): +def test_import_help(): from umleditor.mvc_model.help_command import help_menu assert help_menu -def test_cli_lexer_import(): - from umleditor.mvc_view.cli_lexer import _command_flag_map, _command_function_map +def test_import_cli_lexer(): + from umleditor.mvc_controller.uml_lexer import _command_flag_map, _command_function_map, lex_input assert _command_flag_map - assert _command_function_map \ No newline at end of file + assert _command_function_map + assert lex_input + +def test_import_cli_controller(): + from umleditor.mvc_controller.cli_controller import CLI_Controller + assert CLI_Controller + +def test_import_gui_controller(): + from umleditor.mvc_controller.gui_controller import ControllerGUI + assert ControllerGUI + +def test_import_class_card(): + from umleditor.mvc_view.gui_view.class_card import ClassCard + assert ClassCard + +def test_import_class_input_dialog(): + from umleditor.mvc_view.gui_view.class_input_dialog import ClassInputDialog + assert ClassInputDialog \ No newline at end of file From 2f5504a65e1ce8813733543de862e7cff5c4f220 Mon Sep 17 00:00:00 2001 From: almostTaklu Date: Sat, 24 Feb 2024 23:11:05 -0500 Subject: [PATCH 041/144] Made some request changes --- src/umleditor/mvc_controller/uml_lexer.py | 4 ++-- src/umleditor/mvc_model/custom_exceptions.py | 17 ++--------------- src/umleditor/mvc_model/diagram.py | 11 ++++++----- src/umleditor/mvc_model/relation.py | 8 ++++++-- 4 files changed, 16 insertions(+), 24 deletions(-) diff --git a/src/umleditor/mvc_controller/uml_lexer.py b/src/umleditor/mvc_controller/uml_lexer.py index 8c564295..80c18ca6 100644 --- a/src/umleditor/mvc_controller/uml_lexer.py +++ b/src/umleditor/mvc_controller/uml_lexer.py @@ -7,7 +7,7 @@ "list" : ["a","c","r","d"], "fld" : ["a","d","r"], "mthd" : ["a","d","r"], - "rel" : ["a","d"], + "rel" : ["a","t","d"], "save" : [""], "load" : [""], "exit" : [""], @@ -22,7 +22,7 @@ "list" : ["list_everything","list_entities","list_relations","list_entity_details"], "fld" : ["add_field","delete_field","rename_field"], "mthd" : ["add_method","delete_method","rename_method"], - "rel" : ["add_relation","delete_relation"], + "rel" : ["add_relation","change_relation_type","delete_relation"], "save" : ["save"], "load" : ["load"], "exit" : ["quit"], diff --git a/src/umleditor/mvc_model/custom_exceptions.py b/src/umleditor/mvc_model/custom_exceptions.py index d17c7f63..4703b922 100644 --- a/src/umleditor/mvc_model/custom_exceptions.py +++ b/src/umleditor/mvc_model/custom_exceptions.py @@ -86,21 +86,8 @@ class InvalidRelationTypeError(Error): being added. """ - def __init__(self, source, destination): - super().__init__(f"Relation between '{source} -> {destination}' has invalid type.") - - class InvalidRelationNewTypeError(Error): - """ - Exception raised when the relation being added has invalid new type. - - Args: - source (Entity): The source of the relation that was being added. - destination (Entity): The destination of the relation that was - being added. - - """ - def __init__(self, source, destination): - super().__init__(f"Relation between '{source} -> {destination}' has invalid new type.") + def __init__(self, invalid_type): + super().__init__(f"{invalid_type} is not a valid relation type.") #===============================================================================# #Method Exceptions diff --git a/src/umleditor/mvc_model/diagram.py b/src/umleditor/mvc_model/diagram.py index eb765e02..46989a09 100644 --- a/src/umleditor/mvc_model/diagram.py +++ b/src/umleditor/mvc_model/diagram.py @@ -185,7 +185,7 @@ def add_relation(self,source, destination, type): """ # Check for valid relationship type if type not in Relation.RELATIONSHIP_TYPE: - raise CustomExceptions.InvalidRelationTypeError(source, destination) + raise CustomExceptions.InvalidRelationTypeError(type) # Check for valid source and destination if source not in self._entities: @@ -235,20 +235,21 @@ def change_relation_type(self, source, destination, new_type): Args: source (str): The name of the source entity. destination (str): The name of the destination entity. - new_type (str): The new type of the relation. + new_type (str): The new type of the relation to change. Raises: CustomExceptions.EntityNotFoundError: If either the source or the destination entity is not found. CustomExceptions.RelationDoesNotExistError: If a relation does not exist between the source and destination entities. - CustomExceptions.InvalidRelationTypeError: If the relation type is - not valid. + CustomExceptions.InvalidRelationTypeError: If the relation type to + is change not valid. """ # Check for valid relationship type if new_type not in Relation.RELATIONSHIP_TYPE: - raise CustomExceptions.InvalidRelationNewTypeError(source, destination) + raise CustomExceptions.InvalidRelationTypeError(new_type) + # Check for valid source and destination for rel in self._relations: if rel.get_source() == self._entities[source] and rel.get_destination() == self._entities[destination]: rel._type = new_type diff --git a/src/umleditor/mvc_model/relation.py b/src/umleditor/mvc_model/relation.py index fcf14be6..855e3162 100644 --- a/src/umleditor/mvc_model/relation.py +++ b/src/umleditor/mvc_model/relation.py @@ -1,4 +1,5 @@ from .entity import Entity +from .custom_exceptions import CustomExceptions class Relation: RELATIONSHIP_TYPE = {'aggregation', 'composition', 'inheritance', 'realization'} @@ -10,15 +11,18 @@ def __init__(self, type, source=Entity(), destination=Entity()): Args: source (Entity): The entity at the start of the relation. destination (Entity): The entity at the end of the relation. + type (str): The type of the relation. Raises: - None. + CustomExceptions.InvalidRelationTypeError: If the type of the relation is + not valid. Returns: None. """ + # check if the type is valid if type not in self.RELATIONSHIP_TYPE: - raise ValueError(f"Invalid relationship type: {type}") + raise CustomExceptions.InvalidRelationTypeError(type) self._source = source self._destination = destination From 1610d42f22853c2e53bbcac4c0c2d93c286ec4ff Mon Sep 17 00:00:00 2001 From: Peter F Date: Sun, 25 Feb 2024 00:30:33 -0500 Subject: [PATCH 042/144] Bugfixes - Removed redundant custom exceptions - Hitting enter with no more input no longer generates a blank line in the CLI --- src/umleditor/mvc_controller/controller.py | 1 + src/umleditor/mvc_controller/uml_parser.py | 5 +---- src/umleditor/mvc_model/custom_exceptions.py | 20 +++----------------- 3 files changed, 5 insertions(+), 21 deletions(-) diff --git a/src/umleditor/mvc_controller/controller.py b/src/umleditor/mvc_controller/controller.py index e329b832..3a1c989f 100644 --- a/src/umleditor/mvc_controller/controller.py +++ b/src/umleditor/mvc_controller/controller.py @@ -14,6 +14,7 @@ def __init__(self, d:Diagram = Diagram(), q:bool = False) -> None: self._diagram = d def run(self, line:str) -> str: + if(len(line.strip()) > 0): try: #parse the command input = parse(self, line) diff --git a/src/umleditor/mvc_controller/uml_parser.py b/src/umleditor/mvc_controller/uml_parser.py index e98651d5..4076c6a0 100644 --- a/src/umleditor/mvc_controller/uml_parser.py +++ b/src/umleditor/mvc_controller/uml_parser.py @@ -21,9 +21,6 @@ def parse (c, input:str) -> list: With invalid flag: CustomExceptions.InvalidFlagError With invalid command: CustomExceptions.CommandNotFoundError ''' - #guarding no input saves resources - if not input: - raise CE.NoInputError() #actual input to be parsed, split on spaces bits = input.split() @@ -50,7 +47,7 @@ def parse (c, input:str) -> list: elif command_class == Entity: #if no args were provided, no entity can be found. Generate an error about invalid args if not args: - raise CE.NoArgsGivenError() + raise CE.NeedsMoreInput() #if the method is in entity, get entity that needs to be changed #pop the first element of args because it is the entity name, not a method param diff --git a/src/umleditor/mvc_model/custom_exceptions.py b/src/umleditor/mvc_model/custom_exceptions.py index 4703b922..d5324649 100644 --- a/src/umleditor/mvc_model/custom_exceptions.py +++ b/src/umleditor/mvc_model/custom_exceptions.py @@ -168,13 +168,13 @@ class NoEntitySelected(Error): command (str): The name of the command that was called with the invalid flag. """ def __init__(self) -> None: - super().__init__(f"No class selected. Use 'class -s name' to select a class.") + super().__init__(f"No class selected.") class CommandNotFoundError(Error): """Exception raised when an invalid command is entered""" def __init__(self, name) -> None: - super().__init__(f"Command '{name}' does not exist. Try again or type 'help' for help.") + super().__init__(f"Command '{name}' does not exist.") class InvalidArgCountError(Error): ''' @@ -193,27 +193,13 @@ def __init__(self, error): super().__init__(f"{match.group(1)} too few arguments given.") else: super().__init__(f"Expected {match.group(1)} arguments, but {match.group(2)} were given.") - - class NoArgsGivenError(Error): - ''' - Exception raised when a user gives no args to a command that requires one to be parsed - ''' - def __init__(self): - super().__init__(f"This command requires additional input. Type 'help' for command usage.") - - class NoInputError(Error): - ''' - Exception raised when no input is given and enter is hit - ''' - def __init__(self): - super().__init__(f"") class NeedsMoreInput(Error): ''' Exception raised when user gives only a command name ''' def __init__(self): - super().__init__(f"This command requires more input. Please try again or type 'help' for command usage.") + super().__init__(f"This command requires more input.") #===============================================================================# #I/O Exceptions #===============================================================================# From 2964dd306ecb82a26122d1d1f9bd1da88d5e7461 Mon Sep 17 00:00:00 2001 From: Peter F Date: Sun, 25 Feb 2024 12:01:14 -0500 Subject: [PATCH 043/144] removed two parens --- src/umleditor/mvc_controller/controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/umleditor/mvc_controller/controller.py b/src/umleditor/mvc_controller/controller.py index 3a1c989f..753a6393 100644 --- a/src/umleditor/mvc_controller/controller.py +++ b/src/umleditor/mvc_controller/controller.py @@ -14,7 +14,7 @@ def __init__(self, d:Diagram = Diagram(), q:bool = False) -> None: self._diagram = d def run(self, line:str) -> str: - if(len(line.strip()) > 0): + if len(line.strip()) > 0: try: #parse the command input = parse(self, line) From 22a249b5b0e2610a5f09cdd652ff6597482a19f2 Mon Sep 17 00:00:00 2001 From: AdamG-L Date: Sun, 25 Feb 2024 15:04:40 -0500 Subject: [PATCH 044/144] Generalized click method --- main.py | 4 ++-- src/umleditor/mvc_view/gui_view/class_card.py | 20 +++++++++++-------- src/umleditor/mvc_view/gui_view/view_GUI.py | 4 ++-- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/main.py b/main.py index 403e2f4e..cf10f68e 100644 --- a/main.py +++ b/main.py @@ -49,5 +49,5 @@ def mainGUI(): if not __debug__: debug_main() else: - main() - #mainGUI() + #main() + mainGUI() diff --git a/src/umleditor/mvc_view/gui_view/class_card.py b/src/umleditor/mvc_view/gui_view/class_card.py index 6531d5cc..7eca0ed8 100644 --- a/src/umleditor/mvc_view/gui_view/class_card.py +++ b/src/umleditor/mvc_view/gui_view/class_card.py @@ -101,7 +101,7 @@ def show_class_menu(self, position): """ menu = QMenu() field_action = QAction("Add Field", self) - field_action.triggered.connect(self.add_field_clicked) + field_action.triggered.connect(lambda: self.menu_action_clicked(self._list_field)) menu.addAction(field_action) menu.exec(self._class_label.mapToGlobal(position)) @@ -114,19 +114,22 @@ def show_field_menu(self, position): """ pass - def add_field_clicked(self): + def menu_action_clicked(self, list: QListWidget): """ Adds a field when the "Add Field" action is clicked. """ # Disables unselected interactions - self._enable_widgets_signal.emit(False, self) + self._enable_widgets_signal.emit(False, self) + self.disable_context_menus() + # Create field and add to list item = QListWidgetItem() - self._list_field.addItem(item) + list.addItem(item) #!!! + field_text = QLineEdit() self._selected_line = field_text # lambda ensures text is only evaluated on enter - field_text.returnPressed.connect(lambda: self.verify_input(field_text.text(), field_text)) + field_text.returnPressed.connect(lambda: self.verify_input(field_text.text(), list)) # Formatting / Style field_text.setStyleSheet("background-color: #ADD8E6;") self._list_field.setItemWidget(item, field_text) @@ -134,7 +137,7 @@ def add_field_clicked(self): field_text.setAlignment(Qt.AlignmentFlag.AlignCenter) field_text.setFocus() - def disable_unselected_items(self): + def disable_context_menus(self): """ Disables context menus for all items within the ClassCard """ @@ -152,7 +155,7 @@ def enable_all_items(self): self._list_method.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self._list_relation.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) - def verify_input(self, input: str, widget: QWidget): + def verify_input(self, input: str, list: QListWidget): """ Sends a signal for the task to be processed. @@ -160,7 +163,8 @@ def verify_input(self, input: str, widget: QWidget): input (str): The input text. widget (QWidget): The associated ClassCard widget. """ - task = "fld -a " + self._class_label.text() + " " + input + if list == self._list_field: + task = "fld -a " + self._class_label.text() + " " + input self._process_task_signal.emit(task, self) def get_selected_line(self): diff --git a/src/umleditor/mvc_view/gui_view/view_GUI.py b/src/umleditor/mvc_view/gui_view/view_GUI.py index 5a065edd..97f4a99b 100644 --- a/src/umleditor/mvc_view/gui_view/view_GUI.py +++ b/src/umleditor/mvc_view/gui_view/view_GUI.py @@ -104,6 +104,6 @@ def enable_widgets(self, enabled: bool, active_widget: QWidget): child_widget.setEnabled(True) else: child_widget.setEnabled(False) - if isinstance(child_widget, ClassCard): - active_widget.disable_unselected_items() + #if isinstance(active_widget, ClassCard): + # active_widget.disable_context_menus() From f71bcf5393f704c37303f1d7378f1141de93435b Mon Sep 17 00:00:00 2001 From: AdamG-L Date: Sun, 25 Feb 2024 15:46:58 -0500 Subject: [PATCH 045/144] Method Functionality (TODO Commented out) --- src/umleditor/mvc_view/gui_view/class_card.py | 28 ++++++++++++------- src/umleditor/mvc_view/gui_view/view_GUI.py | 2 -- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/umleditor/mvc_view/gui_view/class_card.py b/src/umleditor/mvc_view/gui_view/class_card.py index 7eca0ed8..4d3c6dda 100644 --- a/src/umleditor/mvc_view/gui_view/class_card.py +++ b/src/umleditor/mvc_view/gui_view/class_card.py @@ -99,10 +99,16 @@ def show_class_menu(self, position): Args: position: The position of the context menu. """ + # Create menu & Actions menu = QMenu() field_action = QAction("Add Field", self) - field_action.triggered.connect(lambda: self.menu_action_clicked(self._list_field)) + # TODO method_action = QAction("Add Method", self) menu.addAction(field_action) + # TODO menu.addAction(method_action) + # Add button functionality + field_action.triggered.connect(lambda: self.menu_action_clicked(self._list_field, "Enter Field")) + # TODO method_action.triggered.connect(lambda: self.menu_action_clicked(self._list_method, "e.g. add(int, int)")) + # Create Menu menu.exec(self._class_label.mapToGlobal(position)) def show_field_menu(self, position): @@ -114,7 +120,7 @@ def show_field_menu(self, position): """ pass - def menu_action_clicked(self, list: QListWidget): + def menu_action_clicked(self, list: QListWidget, placeholder: str): """ Adds a field when the "Add Field" action is clicked. """ @@ -126,16 +132,15 @@ def menu_action_clicked(self, list: QListWidget): item = QListWidgetItem() list.addItem(item) #!!! - field_text = QLineEdit() - self._selected_line = field_text + text = QLineEdit() + self._selected_line = text # lambda ensures text is only evaluated on enter - field_text.returnPressed.connect(lambda: self.verify_input(field_text.text(), list)) + text.returnPressed.connect(lambda: self.verify_input(text.text(), list)) # Formatting / Style - field_text.setStyleSheet("background-color: #ADD8E6;") - self._list_field.setItemWidget(item, field_text) - field_text.setPlaceholderText("Enter Field Here") - field_text.setAlignment(Qt.AlignmentFlag.AlignCenter) - field_text.setFocus() + text.setStyleSheet("background-color: #ADD8E6;") + list.setItemWidget(item, text) + text.setPlaceholderText(placeholder) + text.setAlignment(Qt.AlignmentFlag.AlignCenter) def disable_context_menus(self): """ @@ -165,6 +170,9 @@ def verify_input(self, input: str, list: QListWidget): """ if list == self._list_field: task = "fld -a " + self._class_label.text() + " " + input + elif list == self._list_method: + # TODO given e.g. class add(one, two) run this command + pass self._process_task_signal.emit(task, self) def get_selected_line(self): diff --git a/src/umleditor/mvc_view/gui_view/view_GUI.py b/src/umleditor/mvc_view/gui_view/view_GUI.py index 97f4a99b..00e097e5 100644 --- a/src/umleditor/mvc_view/gui_view/view_GUI.py +++ b/src/umleditor/mvc_view/gui_view/view_GUI.py @@ -104,6 +104,4 @@ def enable_widgets(self, enabled: bool, active_widget: QWidget): child_widget.setEnabled(True) else: child_widget.setEnabled(False) - #if isinstance(active_widget, ClassCard): - # active_widget.disable_context_menus() From 7178652d566cdfd66e7b99a391728e273b4c71d4 Mon Sep 17 00:00:00 2001 From: AdamG-L Date: Sun, 25 Feb 2024 16:19:15 -0500 Subject: [PATCH 046/144] Commented relation impl. --- src/umleditor/mvc_view/gui_view/class_card.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/umleditor/mvc_view/gui_view/class_card.py b/src/umleditor/mvc_view/gui_view/class_card.py index 4d3c6dda..36a16342 100644 --- a/src/umleditor/mvc_view/gui_view/class_card.py +++ b/src/umleditor/mvc_view/gui_view/class_card.py @@ -103,11 +103,16 @@ def show_class_menu(self, position): menu = QMenu() field_action = QAction("Add Field", self) # TODO method_action = QAction("Add Method", self) + # TODO relation_action = QAction("Add Relation", self) + menu.addAction(field_action) # TODO menu.addAction(method_action) + # TODO menu.addAction(relation_action) + # Add button functionality field_action.triggered.connect(lambda: self.menu_action_clicked(self._list_field, "Enter Field")) # TODO method_action.triggered.connect(lambda: self.menu_action_clicked(self._list_method, "e.g. add(int, int)")) + # TODO relation_action.triggered.connect(lambda: self.menu_action_clicked(self._list_field, "Enter Field")) # Create Menu menu.exec(self._class_label.mapToGlobal(position)) From e9e67ea3f5698358443dcf04aeef24372b8702c7 Mon Sep 17 00:00:00 2001 From: Marshall Feng Date: Sun, 25 Feb 2024 16:49:12 -0500 Subject: [PATCH 047/144] Redirect save/load folder to the root directory of the project --- src/umleditor/mvc_controller/controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/umleditor/mvc_controller/controller.py b/src/umleditor/mvc_controller/controller.py index 753a6393..cbdb10b2 100644 --- a/src/umleditor/mvc_controller/controller.py +++ b/src/umleditor/mvc_controller/controller.py @@ -65,7 +65,7 @@ def save(self, name: str) -> None: #### Parameters: - `name` (str): The name of the file to be saved. ''' - path = os.path.join(os.path.dirname(__file__), 'save') + path = os.path.join(os.path.dirname(__file__), '../', '../', '../', 'save') if not os.path.exists(path): os.makedirs(path) path = os.path.join(path, name + '.json') @@ -78,7 +78,7 @@ def load(self, name: str) -> None: #### Parameters: - `path` (str): The name of the file to be loaded. ''' - path = os.path.join(os.path.dirname(__file__), 'save') + path = os.path.join(os.path.dirname(__file__), '../', '../', '../', 'save') if not os.path.exists(path): os.makedirs(path) path = os.path.join(path, name + '.json') From ec00a04f8b50681251e1608509d630d64150fcd6 Mon Sep 17 00:00:00 2001 From: Marshall Feng Date: Sun, 25 Feb 2024 17:00:21 -0500 Subject: [PATCH 048/144] Not sure why the lib64 folder is never ignored by git, ignore .venv folder instead to ignore everything in .venv directory --- .gitignore | 1 + lib64 | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 120000 lib64 diff --git a/.gitignore b/.gitignore index ab8a3e31..4d7daaae 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ venv.cfg pyvenv.cfg #Linux venv directories +.venv/ bin/ include/ lib/ diff --git a/lib64 b/lib64 deleted file mode 120000 index 7951405f..00000000 --- a/lib64 +++ /dev/null @@ -1 +0,0 @@ -lib \ No newline at end of file From a4460215a2f9f62a91fe1bf3d027fa9f945bf306 Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 25 Feb 2024 17:25:16 -0500 Subject: [PATCH 049/144] Help Menu Updates --- src/umleditor/mvc_model/help_command.py | 26 ++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/umleditor/mvc_model/help_command.py b/src/umleditor/mvc_model/help_command.py index c380e3f7..a158e4c1 100644 --- a/src/umleditor/mvc_model/help_command.py +++ b/src/umleditor/mvc_model/help_command.py @@ -14,21 +14,29 @@ def help_menu(): """ menu = ( #A general description - "\nHelp menu: For the best view, resize your window so that this message and the bar at the end are on one line. |\n\n" + "\nHelp menu: For the best view, resize your window so that this message and the bar at the end are on one line. |\n\n" "Below are the commands you can call and an explanation of what each does. Anything inside single quotes is decided\n" "by you! Enter the command, replacing anything in the single quotes, and the quotes themselves, with the name you\n" "want to use.\n\n" "A valid name is made up of any combination of letters and numbers.\n\n" #Class Commands - "Class Commands: \n\t" - "class -a 'name' - adds a class with name 'name'. Cannot add classes with duplicate or invalid names\n\t" - "class -d 'name' - deletes a class with name 'name'\n\t" + "Class Commands:\n\t" + "class -a 'name' - adds a class with name 'name'. Cannot add classes with duplicate or invalid names\n\t" + "class -d 'name' - deletes a class with name 'name'\n\t" "class -r 'old' 'new' - renames class 'old' to 'new'. Cannot rename classes to duplicate or invalid names\n" - #Attribute Commands - "Attribute Commands: \n\t" - "att -a class 'name' - adds an attribute with name 'name' to class 'class'\n\t" - "att -d class 'name' - deletes an attribute with name 'name' from class 'class' if one exists\n\t" - "att -r class 'old' 'new' - renames an attribute from name 'old' to name 'new' in class 'class'\n" + #Field Commands + "Field Commands: \n\t" + "fld -a 'class' 'name' - adds a field with name 'name' to class 'class'\n\t" + "fld -d 'class' 'name' - deletes a field with name 'name' from class 'class' if one exists\n\t" + "fld -r 'class' 'old' 'new' - renames a field from name 'old' to name 'new' in class 'class'\n" + #Method Commands + "Method Commands:\n\t" + "mthd -a 'class' 'name' - adds a method with the name 'name' to the class 'class'\n\t" + "mthd -d 'class' 'name' - deletes a method with name 'name' from class 'class' if one exists\n\t" + "mthd -r 'class' 'old 'new' - reames a method form name 'old' to name 'new' in class 'class'\n" + #Paramater Commands + "Parameter Commands:\n\t" + "???\n" #Relation Commands "Relation Commands:\n\t" "rel -a 'src' 'dest' 'type' - adds a relationship between class 'src' and class 'dest' of type 'type'\n\t" From 60b8174d1d46e442b7529818e82237c12a0c4200 Mon Sep 17 00:00:00 2001 From: Marshall Feng Date: Sun, 25 Feb 2024 19:29:28 -0500 Subject: [PATCH 050/144] Refactor serialize function --- src/umleditor/mvc_controller/serializer.py | 61 ++++++++++++++++++---- 1 file changed, 51 insertions(+), 10 deletions(-) diff --git a/src/umleditor/mvc_controller/serializer.py b/src/umleditor/mvc_controller/serializer.py index 069011d0..b2eed0f0 100644 --- a/src/umleditor/mvc_controller/serializer.py +++ b/src/umleditor/mvc_controller/serializer.py @@ -16,7 +16,7 @@ def default(self, obj): from umleditor.mvc_controller.controller_input import read_file, read_line import umleditor.mvc_controller.controller_output as controller_output from umleditor.mvc_model.diagram import Diagram -from umleditor.mvc_model.entity import Entity +from umleditor.mvc_model.entity import Entity, UML_Method from umleditor.mvc_model.relation import Relation from umleditor.mvc_model.custom_exceptions import CustomExceptions as CE @@ -28,16 +28,57 @@ def serialize(diagram: Diagram, path: str) -> None: - `diagram` (Diagram): The diagram object containing entities and relations to be serialized. - `path` (str): The file path where the JSON file will be saved. ''' - entities = {name: vars(obj) for name, obj in diagram._entities.items()} - relations = [] - for x in diagram._relations: - properties = vars(x) - for property_name, property_val in properties.items(): - if isinstance(property_val, Entity): - properties[property_name] = property_val.get_name() - relations.append(properties) + # classes + saved_classes = [] + for entity in diagram._entities.values(): + saved_class = {} + # class name + saved_class['name'] = entity._name + # class fields + saved_fields = [] + for field in entity._fields: + saved_field = {} + # class field name + saved_field['name'] = field + # class field type + saved_field['type'] = 'undefined' #TODO + saved_fields.append(saved_field) + saved_class['fields'] = saved_fields + # class method + saved_methods = [] + for method in entity._methods: + saved_method = {} + # class method name + saved_method['name'] = method._name + # class method return_type + saved_method['return_type'] = 'undefined' #TODO + # class method params + saved_params = [] + for param in method._params: + saved_param = {} + # class method param name + saved_param['name'] = param + # class method param type + saved_param['type'] = 'undefined' # TODO + saved_params.append(saved_param) + saved_method['params'] = saved_params + saved_methods.append(saved_method) + saved_class['methods'] = saved_method + saved_classes.append(saved_class) + # relationships + saved_relationships = [] + for relation in diagram._relations: + saved_relationship = {} + # relationship source + saved_relationship['source'] = relation._source + # relationship destination + saved_relationship['destination'] = relation._destination + # relationship type + saved_relationship['type'] = relation._type + saved_relationships.append(saved_relationship) try: - content = json.dumps(obj={'entities': entities, 'relations': relations}, cls=CustomJSONEncoder) + obj = {'classes': saved_classes, 'relationships': saved_relationships} + content = json.dumps(obj=obj, cls=CustomJSONEncoder) except Exception: raise CE.JsonEncodeError(filepath=path) controller_output.write_file(path=path, content=content) From 943c8e5154b30b74e9fcc63642b9509be3a67a68 Mon Sep 17 00:00:00 2001 From: Marshall Feng Date: Sun, 25 Feb 2024 21:15:45 -0500 Subject: [PATCH 051/144] Refactor deserializer --- src/umleditor/mvc_controller/serializer.py | 78 ++++++++++++++++------ src/umleditor/mvc_model/entity.py | 2 +- src/umleditor/mvc_model/relation.py | 2 +- 3 files changed, 58 insertions(+), 24 deletions(-) diff --git a/src/umleditor/mvc_controller/serializer.py b/src/umleditor/mvc_controller/serializer.py index b2eed0f0..7a0b81df 100644 --- a/src/umleditor/mvc_controller/serializer.py +++ b/src/umleditor/mvc_controller/serializer.py @@ -44,7 +44,7 @@ def serialize(diagram: Diagram, path: str) -> None: saved_field['type'] = 'undefined' #TODO saved_fields.append(saved_field) saved_class['fields'] = saved_fields - # class method + # class methods saved_methods = [] for method in entity._methods: saved_method = {} @@ -63,7 +63,7 @@ def serialize(diagram: Diagram, path: str) -> None: saved_params.append(saved_param) saved_method['params'] = saved_params saved_methods.append(saved_method) - saved_class['methods'] = saved_method + saved_class['methods'] = saved_methods saved_classes.append(saved_class) # relationships saved_relationships = [] @@ -95,30 +95,64 @@ def deserialize(diagram: Diagram, path: str) -> None: - (CustomExceptions.JsonDecodeError): If failed to decode the file - (CustomExceptions.SavedDataError): If file data is not consistent with the Diagram ''' + import traceback content = read_file(path) try: - diagram_attributes = json.loads(content) + obj = json.loads(content) except Exception: raise CE.JsonDecodeError(filepath=path) try: - for attr_name, attr_obj in diagram_attributes.items(): - if attr_name == 'entities': - for name, properties in attr_obj.items(): - entity = Entity() - for property_name, property_val in properties.items(): - if isinstance(getattr(entity, property_name), set): # Because custom encoder save set as list - property_val = set(property_val) - setattr(entity, property_name, property_val) - diagram._entities[name] = entity - elif attr_name == 'relations': - for properties in attr_obj: - relation = Relation() - for property_name, property_val in properties.items(): - if isinstance(getattr(relation, property_name), Entity): - property_val = diagram._entities[property_val] - if isinstance(getattr(relation, property_name), set): # Because custom encoder save set as list - property_val = set(property_val) - setattr(relation, property_name, property_val) - diagram._relations.append(relation) + # classes + loaded_classes = {} + for saved_class in obj['classes']: + loaded_class = Entity() + # class name + loaded_class._name = saved_class['name'] + # class fields + loaded_fields = [] + for saved_field in saved_class['fields']: + loaded_field = str() # str is the type of field + # class field name + loaded_field = saved_field['name'] + # class field type + # TODO: field type unused + loaded_fields.append(loaded_field) + loaded_class._fields = loaded_fields + # class methods + loaded_methods = [] + for saved_method in saved_class['methods']: + loaded_method = UML_Method() + # class method name + loaded_method._name = saved_method['name'] + # class method return_type + # TODO: return_type unused + # class method params + loaded_params = [] + for saved_param in saved_method['params']: + loaded_param = str() # str is the type of param + # class method param name + loaded_param = saved_param['name'] + # class method param type + # TODO: type unused + loaded_params.append(loaded_param) + loaded_method._params = loaded_params + loaded_methods.append(loaded_method) + loaded_class._methods = loaded_methods + loaded_classes[loaded_class._name] = loaded_class + diagram._entities = loaded_classes + # relationships + loaded_relationships = [] + for saved_relationship in obj['relationships']: + loaded_relationship = Relation() + # relationship source + loaded_relationship._source = saved_relationship['source'] + # relationship destination + loaded_relationship._destination = saved_relationship['destination'] + # relationship type + loaded_relationship._type = saved_relationship['type'] + loaded_relationships.append(loaded_relationship) + diagram._relations = loaded_relationships except Exception: + traceback.print_exc() + traceback.print_exception() raise CE.SavedDataError(filepath=path) \ No newline at end of file diff --git a/src/umleditor/mvc_model/entity.py b/src/umleditor/mvc_model/entity.py index ac69eff1..bcc05629 100644 --- a/src/umleditor/mvc_model/entity.py +++ b/src/umleditor/mvc_model/entity.py @@ -169,7 +169,7 @@ def __str__(self) -> str: return self._name class UML_Method: - def __init__(self, method_name): + def __init__(self, method_name=''): """ Creates a UML_Method object. diff --git a/src/umleditor/mvc_model/relation.py b/src/umleditor/mvc_model/relation.py index 855e3162..71c245be 100644 --- a/src/umleditor/mvc_model/relation.py +++ b/src/umleditor/mvc_model/relation.py @@ -4,7 +4,7 @@ class Relation: RELATIONSHIP_TYPE = {'aggregation', 'composition', 'inheritance', 'realization'} - def __init__(self, type, source=Entity(), destination=Entity()): + def __init__(self, type=next(iter(RELATIONSHIP_TYPE)), source=Entity(), destination=Entity()): """ Creates a relation between a source entity to a destination entity. From 19e11368daf491ddf644033bc3dd231137612de5 Mon Sep 17 00:00:00 2001 From: Marshall Feng Date: Sun, 25 Feb 2024 21:17:11 -0500 Subject: [PATCH 052/144] Remove debug code --- src/umleditor/mvc_controller/serializer.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/umleditor/mvc_controller/serializer.py b/src/umleditor/mvc_controller/serializer.py index 7a0b81df..0d75f73e 100644 --- a/src/umleditor/mvc_controller/serializer.py +++ b/src/umleditor/mvc_controller/serializer.py @@ -95,7 +95,6 @@ def deserialize(diagram: Diagram, path: str) -> None: - (CustomExceptions.JsonDecodeError): If failed to decode the file - (CustomExceptions.SavedDataError): If file data is not consistent with the Diagram ''' - import traceback content = read_file(path) try: obj = json.loads(content) @@ -153,6 +152,4 @@ def deserialize(diagram: Diagram, path: str) -> None: loaded_relationships.append(loaded_relationship) diagram._relations = loaded_relationships except Exception: - traceback.print_exc() - traceback.print_exception() raise CE.SavedDataError(filepath=path) \ No newline at end of file From fb25e94a802b040f1ff8af039a41b3d9e4a39f05 Mon Sep 17 00:00:00 2001 From: Marshall Feng Date: Sun, 25 Feb 2024 21:19:50 -0500 Subject: [PATCH 053/144] Add _example.json as a save file example --- save/.gitignore | 3 ++- save/_example.json | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 save/_example.json diff --git a/save/.gitignore b/save/.gitignore index c96a04f0..04633fc3 100644 --- a/save/.gitignore +++ b/save/.gitignore @@ -1,2 +1,3 @@ * -!.gitignore \ No newline at end of file +!.gitignore +!_example.json \ No newline at end of file diff --git a/save/_example.json b/save/_example.json new file mode 100644 index 00000000..c45b6f7a --- /dev/null +++ b/save/_example.json @@ -0,0 +1,43 @@ +{ +"classes": [ + { + "name": "Tire", + "fields": [ + { "name": "diameter", "type": "float" }, + { "name": "psi", "type": "float" }, + { "name": "brand", "type": "string" } + ], + "methods": [ + { + "name": "setPSI", + "return_type" : "void", + "params": [ + { "name": "new_psi", "type": "string" } + ] + } + ] + }, + { + "name": "Car", + "fields" : [ + { "name": "make", "type": "string" }, + { "name": "model", "type": "string" }, + { "name": "year", "type": "int" } + ], + "methods": [ + { + "name" : "drive", + "return_type" : "void", + "params": [] + } + ] + } +], +"relationships": [ + { + "source": "Tire", + "destination": "Car", + "type": "composition" + } +] +} \ No newline at end of file From 6ff8838ddc380a7892ac47c0a346a42e5ed87faf Mon Sep 17 00:00:00 2001 From: Marshall Feng Date: Sun, 25 Feb 2024 22:07:49 -0500 Subject: [PATCH 054/144] Fix bugs of deserializer loading source/destination as str instead of Entity --- src/umleditor/mvc_controller/serializer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/umleditor/mvc_controller/serializer.py b/src/umleditor/mvc_controller/serializer.py index 0d75f73e..eecde1e3 100644 --- a/src/umleditor/mvc_controller/serializer.py +++ b/src/umleditor/mvc_controller/serializer.py @@ -144,9 +144,9 @@ def deserialize(diagram: Diagram, path: str) -> None: for saved_relationship in obj['relationships']: loaded_relationship = Relation() # relationship source - loaded_relationship._source = saved_relationship['source'] + loaded_relationship._source = loaded_classes[saved_relationship['source']] # relationship destination - loaded_relationship._destination = saved_relationship['destination'] + loaded_relationship._destination = loaded_classes[saved_relationship['destination']] # relationship type loaded_relationship._type = saved_relationship['type'] loaded_relationships.append(loaded_relationship) From 67f80666afa3bcd6dd0697348fd3c7cb135c5325 Mon Sep 17 00:00:00 2001 From: Peter Date: Mon, 26 Feb 2024 10:37:39 -0500 Subject: [PATCH 055/144] Build Script - Updated requirements in setup.py - Created build script - removed a blank line in main.py that didn't need to be there --- build.py | 27 +++++++++++++++++++++++++++ main.py | 1 - setup.py | 5 +++++ 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 build.py diff --git a/build.py b/build.py new file mode 100644 index 00000000..cd00c947 --- /dev/null +++ b/build.py @@ -0,0 +1,27 @@ +import os + +def main(): + if in_venv(): + os.system("pip install -e .") + os.system("pyinstaller main.py") + +def in_venv() -> bool: + '''Checks if the user is currently in a virtual environment + + Return: + True - the user is in a venv or wants to continue outside one + False - the user is not in a venv and doesn't want to continue + ''' + venv_exists = True + if not os.getenv("VIRTUAL_ENV"): + print('''You are not currently in a virtual environment. + Running this script outside a virtual environment may not work as intended, + \t or may add files to user/system filepaths. For help setting up a venv, check the README. + ''') + venv_exists = True if input("Would you like to continue? Y/[N]: ").strip().lower()[0] == 'y' else False + return venv_exists + +if __name__ == '__main__': + main() + + diff --git a/main.py b/main.py index 403e2f4e..d1676afb 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,4 @@ from umleditor.mvc_controller import Controller - from umleditor.mvc_controller.cli_controller import CLI_Controller from umleditor.mvc_view.gui_view.view_GUI import ViewGUI from umleditor.mvc_controller.gui_controller import ControllerGUI diff --git a/setup.py b/setup.py index 89d16b0a..d9a4027f 100644 --- a/setup.py +++ b/setup.py @@ -13,4 +13,9 @@ packages=find_packages(where='src'), include_package_data=True, package_dir={'': 'src'}, + install_requires = [ + "pyinstaller", + "pyqt6", + "pytest", + ] ) \ No newline at end of file From e361f3749bdd9b0da82db46eb91de1dc3cce93c6 Mon Sep 17 00:00:00 2001 From: Peter Date: Mon, 26 Feb 2024 16:41:46 -0500 Subject: [PATCH 056/144] obligatory DO NOT COMMIT no seriously, something in the parser/lexer is busted rn lol. --- src/test/test_relation.py | 21 ++-- src/umleditor/mvc_model/diagram.py | 150 +++++++++++++--------------- src/umleditor/mvc_model/entity.py | 41 ++++++-- src/umleditor/mvc_model/relation.py | 31 +++++- 4 files changed, 146 insertions(+), 97 deletions(-) diff --git a/src/test/test_relation.py b/src/test/test_relation.py index 7460dadf..e4e468e1 100644 --- a/src/test/test_relation.py +++ b/src/test/test_relation.py @@ -1,26 +1,27 @@ from umleditor.mvc_model import Relation +from umleditor.mvc_model import Entity def test_create_relation(): - rel = Relation("ent1", "ent2") + rel = Relation("aggregation", Entity("ent1"), Entity("ent2")) assert rel def test_get_source(): - rel = Relation("ent1", "ent2") - assert rel.get_source() == "ent1" - assert rel.get_source() != "ent2" + rel = Relation("aggregation", Entity("ent1"), Entity("ent2")) + assert rel.get_source().get_name() == "ent1" + assert rel.get_source().get_name() != "ent2" def test_get_destination(): - rel = Relation("ent1", "ent2") - assert rel.get_destination() != "ent1" - assert rel.get_destination() == "ent2" + rel = Relation("aggregation", Entity("ent1"), Entity("ent2")) + assert rel.get_destination().get_name() != "ent1" + assert rel.get_destination().get_name() == "ent2" def test_contains(): - rel = Relation("ent1", "ent2") + rel = Relation("aggregation", Entity("ent1"), Entity("ent2")) assert rel.contains("ent1") assert rel.contains("ent2") assert not rel.contains("ent3") def test_to_string(): - rel = Relation("ent1", "ent2") - assert str(rel) == "ent1 -> ent2" + rel = Relation("aggregation", Entity("ent1"), Entity("ent2")) + assert str(rel) == "ent1 -> aggregation -> ent2" assert str(rel) != "ent2 -> ent1" \ No newline at end of file diff --git a/src/umleditor/mvc_model/diagram.py b/src/umleditor/mvc_model/diagram.py index a6a0c79c..7781d5e1 100644 --- a/src/umleditor/mvc_model/diagram.py +++ b/src/umleditor/mvc_model/diagram.py @@ -5,8 +5,8 @@ class Diagram: def __init__(self) -> None: - self._entities = {} - self._relations = [] + self._entities:list[Entity] = [] + self._relations:list[Relation] = [] def add_entity(self, name: str): """ @@ -22,10 +22,9 @@ def add_entity(self, name: str): Returns: None """ - if name in self._entities: - raise CustomExceptions.EntityExistsError(name) - else: - self._entities[name] = Entity(name) + if self.has_entity(name): + raise CustomExceptions.EntityExistsError(name) + self._entities[name] = Entity(name) def get_entity(self, name: str): """ @@ -41,11 +40,10 @@ def get_entity(self, name: str): Entity: If the entity exists. None: If the entity does not exist. """ - entity = self._entities.get(name, None) + entity = self._entities[self._entities.index(name)] if self.has_entity(name) else None if entity is None: - raise CustomExceptions.EntityNotFoundError(name) - else: - return entity + raise CustomExceptions.EntityNotFoundError(name) + return entity def delete_entity(self, name: str): @@ -62,22 +60,27 @@ def delete_entity(self, name: str): Returns: None """ - if name not in self._entities: - raise CustomExceptions.EntityNotFoundError(name) + entity = self.get_entity(name) # Check for relations involving the entity and remove them - else: - relations_to_remove = [] - for rel in self._relations: - if rel.contains(self._entities[name]): - relations_to_remove.append(rel) - for rel in relations_to_remove: - self._relations.remove(rel) - - # Remove the entity from the dictionary - del self._entities[name] + relations_to_remove = [] + for rel in self._relations: + if rel.contains(name): + relations_to_remove.append(rel) + for rel in relations_to_remove: + self._relations.remove(rel) + + self._entities.remove(entity) + def has_entity(self, name:str) -> bool: + '''Returns true if the entity exists in this diagram, false otherwise''' + for e in self._entities: + if e.get_name() == name: + return True + return False + + def rename_entity(self, old_name: str, new_name: str): """ Rename a given entity with a new name. @@ -95,16 +98,12 @@ def rename_entity(self, old_name: str, new_name: str): Returns: None """ - if old_name not in self._entities: - raise CustomExceptions.EntityNotFoundError(old_name) - elif new_name in self._entities: - raise CustomExceptions.EntityExistsError(new_name) - # Update key - else: - entity = self._entities[old_name] - entity.set_name(new_name) - self._entities[new_name] = self._entities.pop(old_name) - + ent = self.get_entity(old_name) + + if self.has_entity(new_name): + raise CustomExceptions.EntityExistsError(new_name) + ent.set_name(new_name) + def list_everything(self): """ Returns a representation of the entire diagram. @@ -114,7 +113,7 @@ def list_everything(self): and their relations. """ result = "" - for entity in self._entities.values(): + for entity in self._entities: result += self.list_entity_details(entity.get_name()) + "\n" return result @@ -133,25 +132,16 @@ def list_entity_details(self, entity_name): str: A templated string containing the fields, methods, params and relations of an entity. """ - if not self._entities.__contains__(entity_name): - raise CustomExceptions.EntityNotFoundError(entity_name) - else: - ent = self._entities[entity_name] - fld = ent._fields - rels = [rel for rel in self._relations if rel.contains(ent)] - result = entity_name +":\n" + entity_name + "'s Fields:\n" - fld_string = ', '.join(fld) - result2 = entity_name + "'s Methods:\n" - mthd_string = "" - for m in ent._methods: - mthd_string += ', '.join(str(m) for m in ent._methods) - mthd_string += "\n" - result3 = entity_name + "'s Relations:\n" - rel_string = ', '.join(str(rel) for rel in rels) - return result + fld_string + "\n" + result2 + mthd_string + result3 + rel_string + + ent = self.get_entity(entity_name) + fields = entity_name +":\n" + entity_name + "'s Fields:\n" + ent.list_fields() + '\n' + methods = entity_name + "'s Methods:\n" + ent.list_methods() + relations = entity_name + "'s Relations:\n" + self.list_entity_relations(entity_name) + return fields + methods + relations def list_entities(self): """ + Returns the entities in the relation. Returns: @@ -171,6 +161,18 @@ def list_relations(self): for rel in self._relations: relations_list.append(str(rel)) return '\n'.join(relations_list) + + def list_entity_relations(self, name:str): + ''' Lists all relations that contain a specific entity + + Return: A string containing all relations + ''' + relations_list = [] + for rel in self._relations: + if(rel.contains(name)): + relations_list.append(str(rel)) + return '\n'.join(relations_list) + def add_relation(self,source, destination, type): @@ -190,23 +192,18 @@ def add_relation(self,source, destination, type): CustomExceptions.InvalidRelationTypeError: If the relation type is not valid. """ + src = self.get_entity(source) + dst = self.get_entity(destination) # Check for valid relationship type if type not in Relation.RELATIONSHIP_TYPE: raise CustomExceptions.InvalidRelationTypeError(type) - # Check for valid source and destination - if source not in self._entities: - raise CustomExceptions.EntityNotFoundError(source) - elif destination not in self._entities: - raise CustomExceptions.EntityNotFoundError(destination) - # Check for duplicate relationship containing same source and destination - else: - for rel in self._relations: - if rel.get_source() == self._entities[source] and rel.get_destination() == self._entities[destination]: - raise CustomExceptions.RelationExistsError(source, destination) - # Pass entity objects to relation and add relation to list of existing relations - relationship = Relation(type, self._entities[source], self._entities[destination]) - self._relations.append(relationship) + for rel in self._relations: + to_add = Relation(src, dst, type) + if rel == to_add: + raise CustomExceptions.RelationExistsError(source, destination) + # Pass entity objects to relation and add relation to list of existing relations + self._relations.append(to_add) def delete_relation(self, source, destination): """ @@ -223,18 +220,16 @@ def delete_relation(self, source, destination): exist between the source and destination entities. """ # Check for valid source and destination - if source not in self._entities: - raise CustomExceptions.EntityNotFoundError(source) - elif destination not in self._entities: - raise CustomExceptions.EntityNotFoundError(destination) + src = self.get_entity(source) + dst = self.get_entity(destination) # Look for matching relation to delete - else: - for i, rel in enumerate(self._relations): - if rel.get_source() == self._entities[source] and rel.get_destination() == self._entities[destination]: - del self._relations[i] - return + + for i, rel in enumerate(self._relations): + if rel.get_source() == src and rel.get_destination() == dst: + del self._relations[i] + return raise CustomExceptions.RelationDoesNotExistError(source, destination) - + def change_relation_type(self, source, destination, new_type): """ Changes the type of a relation between two Entities. @@ -252,14 +247,13 @@ def change_relation_type(self, source, destination, new_type): CustomExceptions.InvalidRelationTypeError: If the relation type to is change not valid. """ - # Check for valid relationship type - if new_type not in Relation.RELATIONSHIP_TYPE: - raise CustomExceptions.InvalidRelationTypeError(new_type) - + src = self.get_entity(source) + dst = self.get_entity(destination) + # Check for valid source and destination for rel in self._relations: - if rel.get_source() == self._entities[source] and rel.get_destination() == self._entities[destination]: - rel._type = new_type + if rel.get_source() == src and rel.get_destination() == dst: + rel.set_type(new_type) return raise CustomExceptions.RelationDoesNotExistError(source, destination) diff --git a/src/umleditor/mvc_model/entity.py b/src/umleditor/mvc_model/entity.py index bcc05629..74b0a5ce 100644 --- a/src/umleditor/mvc_model/entity.py +++ b/src/umleditor/mvc_model/entity.py @@ -9,8 +9,8 @@ def __init__(self, entity_name:str=''): entity_name (str): The name of the entity. """ self.set_name(entity_name) - self._fields = [] - self._methods = [] + self._fields:list[str] = [] + self._methods:list[UML_Method] = [] def get_name(self): ''' @@ -91,8 +91,7 @@ def rename_field(self, old_field: str, new_field: str): elif new_field in self._fields: raise CustomExceptions.FieldExistsError(new_field) else: - self._fields.remove(old_field) - self._fields.append(new_field) + self._fields[self._fields.index(old_field)] = new_field def add_method(self, method_name: str): """ @@ -159,6 +158,27 @@ def rename_method(self, old_name: str, new_name: str): if (old_name == um.get_method_name()): um.set_method_name(new_name) + def list_methods(self): + '''Lists all the methods of this entity + + Return: a comma separated list of all methods in this entity + ''' + return ", ".join(str(m) for m in self._methods) + '\n' + + def list_fields(self): + '''Lists all the fields of this entity + + Return: a comma separated list of all methods in this entity + ''' + return ", ".join(str(f) for f in self._fields) + '\n' + + def list_methods(self): + '''Lists all the methods of this entity + + Return: a comma separated list of all methods and their params in this entity + ''' + return ", ".join(str(m) for m in self._methods) + '\n' + def __str__(self) -> str: """ Returns the string representation of the Entity object (its name). @@ -167,6 +187,15 @@ def __str__(self) -> str: str: The name of the entity. """ return self._name + + def __eq__ (self, other): + '''Equality operator for entities + + Return: + True - this and other have the same name + False - this and other do not have the same name + ''' + return self._name == other._name class UML_Method: def __init__(self, method_name=''): @@ -182,8 +211,8 @@ def __init__(self, method_name=''): Returns: None. """ - self._name = method_name - self._params = [] + self._name:str = method_name + self._params:list[str] = [] def get_method_name(self): """ diff --git a/src/umleditor/mvc_model/relation.py b/src/umleditor/mvc_model/relation.py index 71c245be..3f3f5f90 100644 --- a/src/umleditor/mvc_model/relation.py +++ b/src/umleditor/mvc_model/relation.py @@ -58,18 +58,25 @@ def get_destination(self): """ return self._destination - def contains(self, entity: Entity): + def set_type(self, new_type:str): + if type not in self.RELATIONSHIP_TYPE: + raise CustomExceptions.InvalidRelationTypeError(type) + + self._type = new_type + + + def contains(self, name:str): """ Checks if a given entity is part of the relation. Args: - entity (Entity): The entity to be checked. + name(str): The entity to be checked. Returns: bool: Returns True if the entity is the source or the destination of the relation. Returns False if the entity is not in the relation. """ - if entity == self._source or entity == self._destination: + if name == self._source.get_name() or name == self._destination.get_name(): return True else: return False @@ -88,3 +95,21 @@ def __str__(self): str: A string representation of the relation. """ return f'{self._source} -> {self._type} -> {self._destination}' + + def __eq__(self, other): + '''Equality op overload + + Return: + True - all the fields in this == all the fields in other + False - at least one field is different + ''' + if self._source != other._source: + return False + + if self._destination != other._destination: + return False + + if self._type != other._type: + return False + + return True \ No newline at end of file From 58c639ba89720ba9bf12b1ab3203488d4e9ac9eb Mon Sep 17 00:00:00 2001 From: AdamG-L Date: Mon, 26 Feb 2024 16:55:58 -0500 Subject: [PATCH 057/144] Adding edit functionality to individual text rows --- src/umleditor/mvc_view/gui_view/class_card.py | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/umleditor/mvc_view/gui_view/class_card.py b/src/umleditor/mvc_view/gui_view/class_card.py index 36a16342..c8e75a78 100644 --- a/src/umleditor/mvc_view/gui_view/class_card.py +++ b/src/umleditor/mvc_view/gui_view/class_card.py @@ -116,6 +116,25 @@ def show_class_menu(self, position): # Create Menu menu.exec(self._class_label.mapToGlobal(position)) + def show_edit_menu(self, position): + """ + Shows the context menu for the class label. + + Args: + position: The position of the context menu. + """ + # Create menu & Actions + menu = QMenu() + edit_action = QAction("Edit", self) + + menu.addAction(edit_action) + + # Add button functionality + edit_action.triggered.connect(self.edit_action_clicked) + + # Create Menu + menu.exec(self._selected_line.mapToGlobal(position)) + def show_field_menu(self, position): """ Shows the context menu for the field list. @@ -125,9 +144,12 @@ def show_field_menu(self, position): """ pass + def edit_action_clicked(self): + print("Edit action clicked") + def menu_action_clicked(self, list: QListWidget, placeholder: str): """ - Adds a field when the "Add Field" action is clicked. + Adds a field when the "Add ____" action is clicked. """ # Disables unselected interactions self._enable_widgets_signal.emit(False, self) @@ -139,6 +161,8 @@ def menu_action_clicked(self, list: QListWidget, placeholder: str): text = QLineEdit() self._selected_line = text + text.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + text.customContextMenuRequested.connect(self.show_edit_menu) # lambda ensures text is only evaluated on enter text.returnPressed.connect(lambda: self.verify_input(text.text(), list)) # Formatting / Style From 750cf3c28d62c8cd6da0ae41db06413c3cb9a0c7 Mon Sep 17 00:00:00 2001 From: AdamG-L Date: Mon, 26 Feb 2024 17:10:02 -0500 Subject: [PATCH 058/144] Enabling itterative menu management --- .../mvc_controller/gui_controller.py | 2 +- src/umleditor/mvc_view/gui_view/class_card.py | 24 ++++++++++++------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/umleditor/mvc_controller/gui_controller.py b/src/umleditor/mvc_controller/gui_controller.py index 8e25de74..b593ef2e 100644 --- a/src/umleditor/mvc_controller/gui_controller.py +++ b/src/umleditor/mvc_controller/gui_controller.py @@ -69,6 +69,6 @@ def add_field(self, widget): """ widget.get_selected_line().setReadOnly(True) widget.get_selected_line().setStyleSheet("background-color: white;") - widget.enable_all_items() + widget.enable_context_menus(True) self._window.enable_widgets(True, self) diff --git a/src/umleditor/mvc_view/gui_view/class_card.py b/src/umleditor/mvc_view/gui_view/class_card.py index c8e75a78..cea05c8c 100644 --- a/src/umleditor/mvc_view/gui_view/class_card.py +++ b/src/umleditor/mvc_view/gui_view/class_card.py @@ -153,16 +153,15 @@ def menu_action_clicked(self, list: QListWidget, placeholder: str): """ # Disables unselected interactions self._enable_widgets_signal.emit(False, self) - self.disable_context_menus() # Create field and add to list item = QListWidgetItem() list.addItem(item) #!!! - text = QLineEdit() self._selected_line = text text.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) text.customContextMenuRequested.connect(self.show_edit_menu) + # lambda ensures text is only evaluated on enter text.returnPressed.connect(lambda: self.verify_input(text.text(), list)) # Formatting / Style @@ -170,15 +169,24 @@ def menu_action_clicked(self, list: QListWidget, placeholder: str): list.setItemWidget(item, text) text.setPlaceholderText(placeholder) text.setAlignment(Qt.AlignmentFlag.AlignCenter) + + # Disable all context menus while actively editing + self.enable_context_menus(False) - def disable_context_menus(self): + def enable_context_menus(self, enable: bool): """ - Disables context menus for all items within the ClassCard + Enable/disable context menus for all items within the ClassCard """ - self._class_label.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu) - self._list_field.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu) - self._list_method.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu) - self._list_relation.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu) + stack = [self] + while stack: + current_widget = stack.pop() + if enable: + current_widget.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + else: + current_widget.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu) + if isinstance(current_widget, QWidget): + stack.extend(current_widget.findChildren(QWidget)) + def enable_all_items(self): """ From 80516459515786d4e8ea7c178bc87dea7d8c30d4 Mon Sep 17 00:00:00 2001 From: Peter Date: Mon, 26 Feb 2024 17:10:34 -0500 Subject: [PATCH 059/144] STILL NOT WORKING working through an ocean of bugs, lmao its ok tho, it'll be really nice once it's done. Less repetition, fewer points of failure. --- src/umleditor/mvc_model/diagram.py | 10 ++++++---- src/umleditor/mvc_model/entity.py | 6 +++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/umleditor/mvc_model/diagram.py b/src/umleditor/mvc_model/diagram.py index 7781d5e1..9eded11f 100644 --- a/src/umleditor/mvc_model/diagram.py +++ b/src/umleditor/mvc_model/diagram.py @@ -24,7 +24,7 @@ def add_entity(self, name: str): """ if self.has_entity(name): raise CustomExceptions.EntityExistsError(name) - self._entities[name] = Entity(name) + self._entities.append(Entity(name)) def get_entity(self, name: str): """ @@ -40,7 +40,8 @@ def get_entity(self, name: str): Entity: If the entity exists. None: If the entity does not exist. """ - entity = self._entities[self._entities.index(name)] if self.has_entity(name) else None + dummy = Entity(name) + entity = self._entities[self._entities.index(dummy)] if self.has_entity(name) else None if entity is None: raise CustomExceptions.EntityNotFoundError(name) return entity @@ -75,6 +76,7 @@ def delete_entity(self, name: str): def has_entity(self, name:str) -> bool: '''Returns true if the entity exists in this diagram, false otherwise''' + for e in self._entities: if e.get_name() == name: return True @@ -175,7 +177,7 @@ def list_entity_relations(self, name:str): - def add_relation(self,source, destination, type): + def add_relation(self,source:str, destination:str, type:str): """ Adds a relation between two Entities. @@ -198,8 +200,8 @@ def add_relation(self,source, destination, type): if type not in Relation.RELATIONSHIP_TYPE: raise CustomExceptions.InvalidRelationTypeError(type) + to_add = Relation(src, dst, type) for rel in self._relations: - to_add = Relation(src, dst, type) if rel == to_add: raise CustomExceptions.RelationExistsError(source, destination) # Pass entity objects to relation and add relation to list of existing relations diff --git a/src/umleditor/mvc_model/entity.py b/src/umleditor/mvc_model/entity.py index 74b0a5ce..93fe3978 100644 --- a/src/umleditor/mvc_model/entity.py +++ b/src/umleditor/mvc_model/entity.py @@ -8,9 +8,9 @@ def __init__(self, entity_name:str=''): Args: entity_name (str): The name of the entity. """ - self.set_name(entity_name) - self._fields:list[str] = [] - self._methods:list[UML_Method] = [] + self._name:str = entity_name + self._fields: list[str] = [] + self._methods: list[UML_Method] = [] def get_name(self): ''' From cd8282c53b3670d2d03ab46acb500980ff617027 Mon Sep 17 00:00:00 2001 From: AdamG-L Date: Mon, 26 Feb 2024 18:36:45 -0500 Subject: [PATCH 060/144] Added edit detection of selected object --- main.py | 4 +-- src/umleditor/mvc_view/gui_view/class_card.py | 36 ++++++++----------- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/main.py b/main.py index cf10f68e..403e2f4e 100644 --- a/main.py +++ b/main.py @@ -49,5 +49,5 @@ def mainGUI(): if not __debug__: debug_main() else: - #main() - mainGUI() + main() + #mainGUI() diff --git a/src/umleditor/mvc_view/gui_view/class_card.py b/src/umleditor/mvc_view/gui_view/class_card.py index cea05c8c..495d56b8 100644 --- a/src/umleditor/mvc_view/gui_view/class_card.py +++ b/src/umleditor/mvc_view/gui_view/class_card.py @@ -74,9 +74,6 @@ def connect_menus(self): # Connect class label self._class_label.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self._class_label.customContextMenuRequested.connect(self.show_class_menu) - # Connect field list - self._list_field.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) - self._list_field.customContextMenuRequested.connect(self.show_field_menu) def set_styles(self): """ @@ -114,11 +111,11 @@ def show_class_menu(self, position): # TODO method_action.triggered.connect(lambda: self.menu_action_clicked(self._list_method, "e.g. add(int, int)")) # TODO relation_action.triggered.connect(lambda: self.menu_action_clicked(self._list_field, "Enter Field")) # Create Menu - menu.exec(self._class_label.mapToGlobal(position)) + menu.exec(self.mapToGlobal(position)) - def show_edit_menu(self, position): + def show_row_menu(self, position, widget: QLineEdit): """ - Shows the context menu for the class label. + Shows the edit/delete menu for QLineEdit based selections Args: position: The position of the context menu. @@ -126,26 +123,21 @@ def show_edit_menu(self, position): # Create menu & Actions menu = QMenu() edit_action = QAction("Edit", self) - + delete_action = QAction("Delete", self) menu.addAction(edit_action) + menu.addAction(delete_action) # Add button functionality - edit_action.triggered.connect(self.edit_action_clicked) + edit_action.triggered.connect(lambda: self.edit_action_clicked(widget)) # Create Menu - menu.exec(self._selected_line.mapToGlobal(position)) - - def show_field_menu(self, position): - """ - Shows the context menu for the field list. - - Args: - position: The position of the context menu. - """ - pass + menu.exec(self.mapToGlobal(position)) - def edit_action_clicked(self): - print("Edit action clicked") + def edit_action_clicked(self, widget: QLineEdit): + self._old_text = widget.text() + widget.setStyleSheet("background-color: #ADD8E6;") + widget.setReadOnly(False) + widget.setFocus() def menu_action_clicked(self, list: QListWidget, placeholder: str): """ @@ -160,10 +152,12 @@ def menu_action_clicked(self, list: QListWidget, placeholder: str): text = QLineEdit() self._selected_line = text text.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) - text.customContextMenuRequested.connect(self.show_edit_menu) + # Pass the QLineEdit instance + text.customContextMenuRequested.connect(lambda pos: self.show_row_menu(pos, text)) # lambda ensures text is only evaluated on enter text.returnPressed.connect(lambda: self.verify_input(text.text(), list)) + # Formatting / Style text.setStyleSheet("background-color: #ADD8E6;") list.setItemWidget(item, text) From c3c70cdcbdc7630d87fa690590a975dd769ec6df Mon Sep 17 00:00:00 2001 From: Peter F Date: Mon, 26 Feb 2024 18:42:51 -0500 Subject: [PATCH 061/144] List updates - all tests passing - removed duplicate data storage from entities - refactored some code for readability - refactored some code to remove unnecessary checks --- src/umleditor/mvc_model/diagram.py | 21 ++++++++++++++------- src/umleditor/mvc_model/relation.py | 5 ++--- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/umleditor/mvc_model/diagram.py b/src/umleditor/mvc_model/diagram.py index 9eded11f..a2c93b98 100644 --- a/src/umleditor/mvc_model/diagram.py +++ b/src/umleditor/mvc_model/diagram.py @@ -8,6 +8,10 @@ def __init__(self) -> None: self._entities:list[Entity] = [] self._relations:list[Relation] = [] + #===============================================================================# + #Entity Methods + #===============================================================================# + def add_entity(self, name: str): """ Adds an entity with the given name to the Diagram. @@ -106,6 +110,10 @@ def rename_entity(self, old_name: str, new_name: str): raise CustomExceptions.EntityExistsError(new_name) ent.set_name(new_name) + #===============================================================================# + #List Methods + #===============================================================================# + def list_everything(self): """ Returns a representation of the entire diagram. @@ -149,7 +157,7 @@ def list_entities(self): Returns: str: String containing names of all existing entities. """ - entity_names = list(self._entities.keys()) + entity_names = list(str(e) for e in self._entities) return '\n' + ', '.join(entity_names) def list_relations(self): @@ -175,7 +183,9 @@ def list_entity_relations(self, name:str): relations_list.append(str(rel)) return '\n'.join(relations_list) - + #===============================================================================# + #Relation Methods + #===============================================================================# def add_relation(self,source:str, destination:str, type:str): """ @@ -196,11 +206,8 @@ def add_relation(self,source:str, destination:str, type:str): """ src = self.get_entity(source) dst = self.get_entity(destination) - # Check for valid relationship type - if type not in Relation.RELATIONSHIP_TYPE: - raise CustomExceptions.InvalidRelationTypeError(type) - to_add = Relation(src, dst, type) + to_add = Relation(type, src, dst) for rel in self._relations: if rel == to_add: raise CustomExceptions.RelationExistsError(source, destination) @@ -232,7 +239,7 @@ def delete_relation(self, source, destination): return raise CustomExceptions.RelationDoesNotExistError(source, destination) - def change_relation_type(self, source, destination, new_type): + def change_relation_type(self, source:str, destination:str, new_type:str): """ Changes the type of a relation between two Entities. diff --git a/src/umleditor/mvc_model/relation.py b/src/umleditor/mvc_model/relation.py index 3f3f5f90..7116d311 100644 --- a/src/umleditor/mvc_model/relation.py +++ b/src/umleditor/mvc_model/relation.py @@ -2,7 +2,7 @@ from .custom_exceptions import CustomExceptions class Relation: - RELATIONSHIP_TYPE = {'aggregation', 'composition', 'inheritance', 'realization'} + RELATIONSHIP_TYPE = ['aggregation', 'composition', 'inheritance', 'realization'] def __init__(self, type=next(iter(RELATIONSHIP_TYPE)), source=Entity(), destination=Entity()): """ @@ -23,7 +23,6 @@ def __init__(self, type=next(iter(RELATIONSHIP_TYPE)), source=Entity(), destinat # check if the type is valid if type not in self.RELATIONSHIP_TYPE: raise CustomExceptions.InvalidRelationTypeError(type) - self._source = source self._destination = destination self._type = type @@ -59,7 +58,7 @@ def get_destination(self): return self._destination def set_type(self, new_type:str): - if type not in self.RELATIONSHIP_TYPE: + if new_type not in self.RELATIONSHIP_TYPE: raise CustomExceptions.InvalidRelationTypeError(type) self._type = new_type From 9804c5d4223faf16d8c2247464d217352a7ccf22 Mon Sep 17 00:00:00 2001 From: almostTaklu Date: Mon, 26 Feb 2024 19:13:08 -0500 Subject: [PATCH 062/144] Updated pytest for relationship --- src/test/test_relation.py | 46 +++++++++++++------- src/umleditor/mvc_model/custom_exceptions.py | 4 +- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/test/test_relation.py b/src/test/test_relation.py index 7460dadf..1b66b517 100644 --- a/src/test/test_relation.py +++ b/src/test/test_relation.py @@ -1,26 +1,40 @@ -from umleditor.mvc_model import Relation +from umleditor.mvc_model import Relation, Entity def test_create_relation(): - rel = Relation("ent1", "ent2") - assert rel + source = Entity("ent1") + destination = Entity("ent2") + type = next(iter(Relation.RELATIONSHIP_TYPE)) + rel = Relation(type=type, source=source, destination=destination) + assert rel is not None def test_get_source(): - rel = Relation("ent1", "ent2") - assert rel.get_source() == "ent1" - assert rel.get_source() != "ent2" + source = Entity("ent1") + destination = Entity("ent2") + type = next(iter(Relation.RELATIONSHIP_TYPE)) + rel = Relation(type, source, destination) + assert rel.get_source().get_name() == "ent1" + assert rel.get_source().get_name() != "ent2" def test_get_destination(): - rel = Relation("ent1", "ent2") - assert rel.get_destination() != "ent1" - assert rel.get_destination() == "ent2" + source = Entity("ent1") + destination = Entity("ent2") + type = next(iter(Relation.RELATIONSHIP_TYPE)) + rel = Relation(type, source, destination) + assert rel.get_destination().get_name() == "ent2" + assert rel.get_destination().get_name() != "ent1" def test_contains(): - rel = Relation("ent1", "ent2") - assert rel.contains("ent1") - assert rel.contains("ent2") - assert not rel.contains("ent3") + source = Entity("ent1") + destination = Entity("ent2") + type = next(iter(Relation.RELATIONSHIP_TYPE)) + rel = Relation(type, source, destination) + assert rel.contains(source) == True + assert rel.contains(destination) == True + assert rel.contains(Entity("ent3")) == False def test_to_string(): - rel = Relation("ent1", "ent2") - assert str(rel) == "ent1 -> ent2" - assert str(rel) != "ent2 -> ent1" \ No newline at end of file + source = Entity("ent1") + destination = Entity("ent2") + type = "aggregation" + rel = Relation(type=type, source=source, destination=destination) + assert str(rel) == "ent1 -> aggregation -> ent2" \ No newline at end of file diff --git a/src/umleditor/mvc_model/custom_exceptions.py b/src/umleditor/mvc_model/custom_exceptions.py index d5324649..fb2581b6 100644 --- a/src/umleditor/mvc_model/custom_exceptions.py +++ b/src/umleditor/mvc_model/custom_exceptions.py @@ -81,9 +81,7 @@ class InvalidRelationTypeError(Error): Exception raised when the relation being added has no types. Args: - source (Entity): The source of the relation that was being added. - destination (Entity): The destination of the relation that was - being added. + invalid_type (str): The type of the relation that was being added. """ def __init__(self, invalid_type): From e3ca371b976e72aa48280b809cf07de1cbc11728 Mon Sep 17 00:00:00 2001 From: Peter Freedman <104784188+pwfreedm@users.noreply.github.com> Date: Mon, 26 Feb 2024 19:16:01 -0500 Subject: [PATCH 063/144] Delete src/test/test_relation.py Removed this from pr because there is another about it. --- src/test/test_relation.py | 27 --------------------------- 1 file changed, 27 deletions(-) delete mode 100644 src/test/test_relation.py diff --git a/src/test/test_relation.py b/src/test/test_relation.py deleted file mode 100644 index e4e468e1..00000000 --- a/src/test/test_relation.py +++ /dev/null @@ -1,27 +0,0 @@ -from umleditor.mvc_model import Relation -from umleditor.mvc_model import Entity - -def test_create_relation(): - rel = Relation("aggregation", Entity("ent1"), Entity("ent2")) - assert rel - -def test_get_source(): - rel = Relation("aggregation", Entity("ent1"), Entity("ent2")) - assert rel.get_source().get_name() == "ent1" - assert rel.get_source().get_name() != "ent2" - -def test_get_destination(): - rel = Relation("aggregation", Entity("ent1"), Entity("ent2")) - assert rel.get_destination().get_name() != "ent1" - assert rel.get_destination().get_name() == "ent2" - -def test_contains(): - rel = Relation("aggregation", Entity("ent1"), Entity("ent2")) - assert rel.contains("ent1") - assert rel.contains("ent2") - assert not rel.contains("ent3") - -def test_to_string(): - rel = Relation("aggregation", Entity("ent1"), Entity("ent2")) - assert str(rel) == "ent1 -> aggregation -> ent2" - assert str(rel) != "ent2 -> ent1" \ No newline at end of file From a61641292e607ebef9c4bc283a6bfa3afadaf84d Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 26 Feb 2024 19:43:19 -0500 Subject: [PATCH 064/144] Partial work towards Param hookup, and fixing parsing issues. --- src/umleditor/mvc_controller/uml_lexer.py | 4 ++- src/umleditor/mvc_controller/uml_parser.py | 7 ++-- src/umleditor/mvc_model/entity.py | 42 ++++++++++++++++++++-- 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/src/umleditor/mvc_controller/uml_lexer.py b/src/umleditor/mvc_controller/uml_lexer.py index 80c18ca6..8d359e59 100644 --- a/src/umleditor/mvc_controller/uml_lexer.py +++ b/src/umleditor/mvc_controller/uml_lexer.py @@ -7,6 +7,7 @@ "list" : ["a","c","r","d"], "fld" : ["a","d","r"], "mthd" : ["a","d","r"], + "prm" : ["a","d","c"], "rel" : ["a","t","d"], "save" : [""], "load" : [""], @@ -22,7 +23,8 @@ "list" : ["list_everything","list_entities","list_relations","list_entity_details"], "fld" : ["add_field","delete_field","rename_field"], "mthd" : ["add_method","delete_method","rename_method"], - "rel" : ["add_relation","change_relation_type","delete_relation"], + "prm" : ["add_parameters", "remove_parameters", "change_parameters"], + "rel" : ["add_relation", "change_relation_type", "delete_relation"], "save" : ["save"], "load" : ["load"], "exit" : ["quit"], diff --git a/src/umleditor/mvc_controller/uml_parser.py b/src/umleditor/mvc_controller/uml_parser.py index 4076c6a0..1712055d 100644 --- a/src/umleditor/mvc_controller/uml_parser.py +++ b/src/umleditor/mvc_controller/uml_parser.py @@ -5,7 +5,7 @@ import re #list of all classes that need to be searched for commands -classes = [Diagram, Entity, Relation, help_command] +classes = [Diagram, Entity, Relation, UML_Method, help_command] def parse (c, input:str) -> list: @@ -48,10 +48,13 @@ def parse (c, input:str) -> list: #if no args were provided, no entity can be found. Generate an error about invalid args if not args: raise CE.NeedsMoreInput() - #if the method is in entity, get entity that needs to be changed #pop the first element of args because it is the entity name, not a method param obj = c._diagram.get_entity(args.pop(0)) + elif command_class == UML_Method: + if not args: + raise CE.NeedsMoreInput() + obj = c._diagram.get_entity(args.pop(0)).get_method(args.pop(0)) elif command_class == help_command: obj = help_command diff --git a/src/umleditor/mvc_model/entity.py b/src/umleditor/mvc_model/entity.py index bcc05629..58a54c5e 100644 --- a/src/umleditor/mvc_model/entity.py +++ b/src/umleditor/mvc_model/entity.py @@ -13,12 +13,12 @@ def __init__(self, entity_name:str=''): self._methods = [] def get_name(self): - ''' + """ Returns the name of the entity. # Returns: (str): The name of the entity - ''' + """ return self._name def set_name(self, entity_name: str): @@ -94,6 +94,44 @@ def rename_field(self, old_field: str, new_field: str): self._fields.remove(old_field) self._fields.append(new_field) + def has_method(self, method_name:str): + """ + Checks if a method exists inside an entity. + + Args: + method_name (str): The method's name to be checked for. + + Raises: + None. + + Returns: + bool: True if the method exists. False if it does not. + """ + for m in self._methods: + if m.get_method_name() == method_name: + return True + return False + + def get_method(self, method_name:str): + """ + Gets a method object from inside an entity if it exists. + + Args: + method_name (str): The method's name to be gotten from the entity. + + Raises: + CustomExceptions.MethodNotFoundError: If the method does not + exist in the Entity. + + Returns: + method (UML_Method): The method named method_name. + """ + temp = UML_Method(method_name) + method = self._methods[self._methods.index(temp)] if self.has_method(method_name) else None + if method == None: + raise CustomExceptions.MethodNotFoundError(method_name) + return method + def add_method(self, method_name: str): """ Adds a new method to the to the list. From 03a07a485ff9529e0bebb2d94cde0faaca41a10f Mon Sep 17 00:00:00 2001 From: AdamG-L Date: Mon, 26 Feb 2024 19:48:02 -0500 Subject: [PATCH 065/144] Fully functioning Add Field and Edit Field --- main.py | 4 +-- .../mvc_controller/gui_controller.py | 9 ++++- src/umleditor/mvc_controller/uml_lexer.py | 4 +-- src/umleditor/mvc_model/entity.py | 16 +++++++++ src/umleditor/mvc_view/gui_view/class_card.py | 33 +++++++++++++++++-- 5 files changed, 59 insertions(+), 7 deletions(-) diff --git a/main.py b/main.py index 403e2f4e..cf10f68e 100644 --- a/main.py +++ b/main.py @@ -49,5 +49,5 @@ def mainGUI(): if not __debug__: debug_main() else: - main() - #mainGUI() + #main() + mainGUI() diff --git a/src/umleditor/mvc_controller/gui_controller.py b/src/umleditor/mvc_controller/gui_controller.py index b593ef2e..009ca56b 100644 --- a/src/umleditor/mvc_controller/gui_controller.py +++ b/src/umleditor/mvc_controller/gui_controller.py @@ -5,6 +5,7 @@ from PyQt6.QtCore import QDir from umleditor.mvc_controller.controller import Controller from umleditor.mvc_view.gui_view.class_input_dialog import ClassInputDialog +from umleditor.mvc_model.custom_exceptions import CustomExceptions as CE class ControllerGUI (Controller): """ @@ -37,15 +38,19 @@ def run(self, task: str, widget: QtWidgets): task (str): The task to run. widget (QtWidgets): Used to set particular widget to its completed state """ + print(task) try: out = super().run(task) except Exception as e: + # Ignore attempting to delete things that don't exist + if isinstance(e, CE.FieldNotFoundError): + pass self._window.invalid_input_message(str(e)) return # Successful task if "class -a" in task: self.add_class(task, widget) - elif "fld -a" in task: + elif "fld" in task: self.add_field(widget) def add_class(self, task: str, widget: QtWidgets): @@ -67,6 +72,8 @@ def add_field(self, widget): Parameters: widget: The widget instance. """ + print("Add Field Called") + print(widget.get_selected_line().text()) widget.get_selected_line().setReadOnly(True) widget.get_selected_line().setStyleSheet("background-color: white;") widget.enable_context_menus(True) diff --git a/src/umleditor/mvc_controller/uml_lexer.py b/src/umleditor/mvc_controller/uml_lexer.py index 8c564295..ffdb27bf 100644 --- a/src/umleditor/mvc_controller/uml_lexer.py +++ b/src/umleditor/mvc_controller/uml_lexer.py @@ -5,7 +5,7 @@ _command_flag_map = { "class" : ["a","d","r"], "list" : ["a","c","r","d"], - "fld" : ["a","d","r"], + "fld" : ["a","d","r", "e"], "mthd" : ["a","d","r"], "rel" : ["a","d"], "save" : [""], @@ -20,7 +20,7 @@ _command_function_map = { "class" : ["add_entity","delete_entity","rename_entity"], "list" : ["list_everything","list_entities","list_relations","list_entity_details"], - "fld" : ["add_field","delete_field","rename_field"], + "fld" : ["add_field","delete_field","rename_field","edit_field"], "mthd" : ["add_method","delete_method","rename_method"], "rel" : ["add_relation","delete_relation"], "save" : ["save"], diff --git a/src/umleditor/mvc_model/entity.py b/src/umleditor/mvc_model/entity.py index 74001bf3..e8209451 100644 --- a/src/umleditor/mvc_model/entity.py +++ b/src/umleditor/mvc_model/entity.py @@ -48,6 +48,22 @@ def add_field(self, field_name:str): raise CustomExceptions.FieldExistsError(field_name) else: self._fields.append(field_name) + + def edit_field(self, old_field: str, new_field: str): + """ + GUI specific command for editing fields + + Args: + old_field (str): The field' name to be removed + new_field (str): The field' name to be added + """ + if old_field == new_field: + return + else: + self.delete_field(old_field) + self.add_field(new_field) + + def delete_field(self, field_name: str): """ diff --git a/src/umleditor/mvc_view/gui_view/class_card.py b/src/umleditor/mvc_view/gui_view/class_card.py index 495d56b8..46d3e2d3 100644 --- a/src/umleditor/mvc_view/gui_view/class_card.py +++ b/src/umleditor/mvc_view/gui_view/class_card.py @@ -134,10 +134,22 @@ def show_row_menu(self, position, widget: QLineEdit): menu.exec(self.mapToGlobal(position)) def edit_action_clicked(self, widget: QLineEdit): + # Update selected widget + self._selected_line = widget + # Disables unselected interactions + self._enable_widgets_signal.emit(False, self) self._old_text = widget.text() + self.enable_context_menus(False) widget.setStyleSheet("background-color: #ADD8E6;") widget.setReadOnly(False) widget.setFocus() + + def unselected_state(self): + self._enable_widgets_signal.emit(True, self) + self.enable_context_menus(True) + self._selected_line.setReadOnly(True) + self._selected_line.setStyleSheet("background-color: white;") + def menu_action_clicked(self, list: QListWidget, placeholder: str): """ @@ -152,6 +164,10 @@ def menu_action_clicked(self, list: QListWidget, placeholder: str): text = QLineEdit() self._selected_line = text text.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + + # Set old_text as blank + self._old_text = "" + # Pass the QLineEdit instance text.customContextMenuRequested.connect(lambda pos: self.show_row_menu(pos, text)) @@ -199,12 +215,25 @@ def verify_input(self, input: str, list: QListWidget): input (str): The input text. widget (QWidget): The associated ClassCard widget. """ + print(input, self._old_text) if list == self._list_field: - task = "fld -a " + self._class_label.text() + " " + input + ''' + if input == self._old_text: + self.unselected_state() + elif self._old_text == "": + self._process_task_signal.emit("fld -a " + self._class_label.text() + " " + input, self) + else: + self._process_task_signal.emit("fld -d " + self._class_label.text() + " " + self._old_text, self) + self._process_task_signal.emit("fld -a " + self._class_label.text() + " " + input, self) + ''' + if self._old_text == "": + self._process_task_signal.emit("fld -a " + self._class_label.text() + " " + input, self) + else: + self._process_task_signal.emit("fld -e " + self._class_label.text() + " " + self._old_text + " " + input, self) + elif list == self._list_method: # TODO given e.g. class add(one, two) run this command pass - self._process_task_signal.emit(task, self) def get_selected_line(self): """ From 8c49c258bc55e59367b73068d6243c3d321c1c12 Mon Sep 17 00:00:00 2001 From: AdamG-L Date: Mon, 26 Feb 2024 20:36:01 -0500 Subject: [PATCH 066/144] Added delete functionality --- .../mvc_controller/gui_controller.py | 3 +- src/umleditor/mvc_view/gui_view/class_card.py | 49 +++++++++++++++---- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/src/umleditor/mvc_controller/gui_controller.py b/src/umleditor/mvc_controller/gui_controller.py index 009ca56b..f2bc8a37 100644 --- a/src/umleditor/mvc_controller/gui_controller.py +++ b/src/umleditor/mvc_controller/gui_controller.py @@ -50,7 +50,7 @@ def run(self, task: str, widget: QtWidgets): # Successful task if "class -a" in task: self.add_class(task, widget) - elif "fld" in task: + elif "fld -a" in task or "fld -e" in task: self.add_field(widget) def add_class(self, task: str, widget: QtWidgets): @@ -72,7 +72,6 @@ def add_field(self, widget): Parameters: widget: The widget instance. """ - print("Add Field Called") print(widget.get_selected_line().text()) widget.get_selected_line().setReadOnly(True) widget.get_selected_line().setStyleSheet("background-color: white;") diff --git a/src/umleditor/mvc_view/gui_view/class_card.py b/src/umleditor/mvc_view/gui_view/class_card.py index 46d3e2d3..908e7f9e 100644 --- a/src/umleditor/mvc_view/gui_view/class_card.py +++ b/src/umleditor/mvc_view/gui_view/class_card.py @@ -129,11 +129,21 @@ def show_row_menu(self, position, widget: QLineEdit): # Add button functionality edit_action.triggered.connect(lambda: self.edit_action_clicked(widget)) + delete_action.triggered.connect(lambda: self.delete_action_clicked(widget)) # Create Menu menu.exec(self.mapToGlobal(position)) def edit_action_clicked(self, widget: QLineEdit): + """ + Prepares a QLineEdit widget for editing. + + Args: + widget (QLineEdit): The QLineEdit widget to be edited. + + Returns: + None + """ # Update selected widget self._selected_line = widget # Disables unselected interactions @@ -144,7 +154,37 @@ def edit_action_clicked(self, widget: QLineEdit): widget.setReadOnly(False) widget.setFocus() + def delete_action_clicked(self, widget: QLineEdit): + """ + Removes the given QLineEdit widget from its parent QListWidget. + + Args: + widget (QLineEdit): The QLineEdit widget to be removed. + """ + # Delete field from diagram + self._process_task_signal.emit("fld -d " + self._class_label.text() + " " + widget.text(), self) + + lists = [(self._list_field, self._list_field.count()), + (self._list_relation, self._list_relation.count()), + (self._list_method, self._list_method.count())] + + for list_widget, count in lists: + for index in range(count): + item = list_widget.item(index) + if item is not None: + line_edit = list_widget.itemWidget(item) + if line_edit == widget: + list_widget.removeItemWidget(item) + list_widget.takeItem(index) + return + + + + def unselected_state(self): + """ + Returns the Class Card and all widgets to an unselected state + """ self._enable_widgets_signal.emit(True, self) self.enable_context_menus(True) self._selected_line.setReadOnly(True) @@ -217,15 +257,6 @@ def verify_input(self, input: str, list: QListWidget): """ print(input, self._old_text) if list == self._list_field: - ''' - if input == self._old_text: - self.unselected_state() - elif self._old_text == "": - self._process_task_signal.emit("fld -a " + self._class_label.text() + " " + input, self) - else: - self._process_task_signal.emit("fld -d " + self._class_label.text() + " " + self._old_text, self) - self._process_task_signal.emit("fld -a " + self._class_label.text() + " " + input, self) - ''' if self._old_text == "": self._process_task_signal.emit("fld -a " + self._class_label.text() + " " + input, self) else: From 04e1aeed4b81f22afda2a40127395c738d60d630 Mon Sep 17 00:00:00 2001 From: AdamG-L Date: Mon, 26 Feb 2024 20:39:37 -0500 Subject: [PATCH 067/144] Commented out main --- main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index cf10f68e..403e2f4e 100644 --- a/main.py +++ b/main.py @@ -49,5 +49,5 @@ def mainGUI(): if not __debug__: debug_main() else: - #main() - mainGUI() + main() + #mainGUI() From 51a0b83dbff4f166b855adb3cdb4b3f2d5bb593a Mon Sep 17 00:00:00 2001 From: Peter Freedman <104784188+pwfreedm@users.noreply.github.com> Date: Mon, 26 Feb 2024 22:11:17 -0500 Subject: [PATCH 068/144] Removed Duplicate Method --- src/umleditor/mvc_model/entity.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/umleditor/mvc_model/entity.py b/src/umleditor/mvc_model/entity.py index 93fe3978..25e836b5 100644 --- a/src/umleditor/mvc_model/entity.py +++ b/src/umleditor/mvc_model/entity.py @@ -157,13 +157,6 @@ def rename_method(self, old_name: str, new_name: str): for um in self._methods: if (old_name == um.get_method_name()): um.set_method_name(new_name) - - def list_methods(self): - '''Lists all the methods of this entity - - Return: a comma separated list of all methods in this entity - ''' - return ", ".join(str(m) for m in self._methods) + '\n' def list_fields(self): '''Lists all the fields of this entity @@ -329,4 +322,4 @@ def __str__(self): result = self.get_method_name() result += "\n\t" + self.get_method_name() + "'s Params:\n\t\t" param_results = ', '.join(p for p in self._params) - return result + param_results \ No newline at end of file + return result + param_results From 1997fba6b0cd661421b0ff08630dc671dd4acd40 Mon Sep 17 00:00:00 2001 From: Peter Date: Tue, 27 Feb 2024 08:14:40 -0500 Subject: [PATCH 069/144] Updated contains tests between the creation of this file and now, there was a PR that changed contains to take a string instead of an entity. Tests were updated to reflect that change. --- src/test/test_relation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/test_relation.py b/src/test/test_relation.py index 1b66b517..7eb0517a 100644 --- a/src/test/test_relation.py +++ b/src/test/test_relation.py @@ -28,9 +28,9 @@ def test_contains(): destination = Entity("ent2") type = next(iter(Relation.RELATIONSHIP_TYPE)) rel = Relation(type, source, destination) - assert rel.contains(source) == True - assert rel.contains(destination) == True - assert rel.contains(Entity("ent3")) == False + assert rel.contains(source.get_name()) == True + assert rel.contains(destination.get_name()) == True + assert rel.contains("ent3") == False def test_to_string(): source = Entity("ent1") From aa5ace4558d68b8317d7a22ead9bb9135339ff5b Mon Sep 17 00:00:00 2001 From: Peter Date: Tue, 27 Feb 2024 14:31:11 -0500 Subject: [PATCH 070/144] NOT WORKING --- src/umleditor/mvc_controller/controller.py | 6 +-- src/umleditor/mvc_controller/uml_parser.py | 56 +++++++++++++++++----- 2 files changed, 47 insertions(+), 15 deletions(-) diff --git a/src/umleditor/mvc_controller/controller.py b/src/umleditor/mvc_controller/controller.py index cbdb10b2..9df53a52 100644 --- a/src/umleditor/mvc_controller/controller.py +++ b/src/umleditor/mvc_controller/controller.py @@ -6,7 +6,7 @@ from umleditor.mvc_model.diagram import Diagram from umleditor.mvc_controller.uml_parser import check_args import os - +import sys class Controller: def __init__(self, d:Diagram = Diagram(), q:bool = False) -> None: @@ -28,8 +28,8 @@ def run(self, line:str) -> str: except TypeError as t: raise CE.InvalidArgCountError(t) - except ValueError: - raise CE.NeedsMoreInput() + except ValueError as v: + print(str(v)) except Exception as e: raise e diff --git a/src/umleditor/mvc_controller/uml_parser.py b/src/umleditor/mvc_controller/uml_parser.py index 1712055d..1693123c 100644 --- a/src/umleditor/mvc_controller/uml_parser.py +++ b/src/umleditor/mvc_controller/uml_parser.py @@ -1,6 +1,6 @@ from umleditor.mvc_model import CustomExceptions as CE from .uml_lexer import lex_input as lex -from umleditor.mvc_model import * +from umleditor.mvc_model import Diagram, Entity, Relation, UML_Method, help_command import re @@ -29,19 +29,34 @@ def parse (c, input:str) -> list: command_str = "" #list slicing generates an empty list instead of an IndexError command_str = lex(bits[0:1], bits[1:2]) - #Get the args that will be passed to that command + + #Get the class the command is in + command_class = __find_class(command_str) + + obj = c + #UML_Method has enough extra work that needs to be done for it that it's just its own case. + if command_class == UML_Method: + bits = bits[2:] + print("in UML_Method") + #get the object and prep the list for splitting + ent = c._diagram.get_entity(bits.pop(0)) + obj = ent.get_method(bits.pop(0)) + print("before split") + args = __split_list(bits[2:]) + print("past split") + for arg in args: + check_args(arg) + print("above return") + return [getattr(obj, command_str)] + args + + #if the args aren't a list, check them as normal args = [] if not str(bits[1:2]).__contains__("-"): args = check_args(bits[1:]) else: args = check_args(bits[2:]) - #Get the class the command is in - command_class = __find_class(command_str) - #go from knowing which class to having a specific instance - #of the class that the method needs to be called on - obj = c if command_class == Diagram: obj = c._diagram elif command_class == Entity: @@ -51,16 +66,33 @@ def parse (c, input:str) -> list: #if the method is in entity, get entity that needs to be changed #pop the first element of args because it is the entity name, not a method param obj = c._diagram.get_entity(args.pop(0)) - elif command_class == UML_Method: - if not args: - raise CE.NeedsMoreInput() - obj = c._diagram.get_entity(args.pop(0)).get_method(args.pop(0)) elif command_class == help_command: obj = help_command #build and return the callable + args return [getattr(obj, command_str)] + args +def __split_list(args:list[str]) -> list[list[str]]: + '''Splits a list on the delimiter "|" + + Returns + A list of lists containing the lhs and rhs of the "|" + ''' + lhs = [] + rhs = [] + seen_bar = False + for arg in args: + if arg == '|': + seen_bar = True + continue + + rhs.append(arg) if seen_bar else lhs.append(arg) + + if len(rhs) == 0: + return [lhs] + else: + return [lhs, rhs] + def __find_class(function:str): '''Takes a function and locates the class that it exists in @@ -75,7 +107,7 @@ def __find_class(function:str): def check_args(args:list): '''Given a list of args, checks to make sure each one is valid. - Valid is defined as alphanumeric. + Valid is defined as containing alphanumeric chars, underscore, and hyphen. Args: args(list): a list of strings to be checked From 08997fccb9da4de83a0ec7b31308ceb3e06fd8e5 Mon Sep 17 00:00:00 2001 From: AdamG-L Date: Tue, 27 Feb 2024 16:52:41 -0500 Subject: [PATCH 071/144] Removed edit_field (and related files) / Extended functionality on rename_field --- .../mvc_controller/gui_controller.py | 2 +- src/umleditor/mvc_controller/uml_lexer.py | 4 ++-- src/umleditor/mvc_model/entity.py | 20 +++---------------- src/umleditor/mvc_view/gui_view/class_card.py | 2 +- 4 files changed, 7 insertions(+), 21 deletions(-) diff --git a/src/umleditor/mvc_controller/gui_controller.py b/src/umleditor/mvc_controller/gui_controller.py index f2bc8a37..3aa94f8b 100644 --- a/src/umleditor/mvc_controller/gui_controller.py +++ b/src/umleditor/mvc_controller/gui_controller.py @@ -50,7 +50,7 @@ def run(self, task: str, widget: QtWidgets): # Successful task if "class -a" in task: self.add_class(task, widget) - elif "fld -a" in task or "fld -e" in task: + elif "fld -a" in task or "fld -r" in task: self.add_field(widget) def add_class(self, task: str, widget: QtWidgets): diff --git a/src/umleditor/mvc_controller/uml_lexer.py b/src/umleditor/mvc_controller/uml_lexer.py index ffdb27bf..8c564295 100644 --- a/src/umleditor/mvc_controller/uml_lexer.py +++ b/src/umleditor/mvc_controller/uml_lexer.py @@ -5,7 +5,7 @@ _command_flag_map = { "class" : ["a","d","r"], "list" : ["a","c","r","d"], - "fld" : ["a","d","r", "e"], + "fld" : ["a","d","r"], "mthd" : ["a","d","r"], "rel" : ["a","d"], "save" : [""], @@ -20,7 +20,7 @@ _command_function_map = { "class" : ["add_entity","delete_entity","rename_entity"], "list" : ["list_everything","list_entities","list_relations","list_entity_details"], - "fld" : ["add_field","delete_field","rename_field","edit_field"], + "fld" : ["add_field","delete_field","rename_field"], "mthd" : ["add_method","delete_method","rename_method"], "rel" : ["add_relation","delete_relation"], "save" : ["save"], diff --git a/src/umleditor/mvc_model/entity.py b/src/umleditor/mvc_model/entity.py index e8209451..227b2fa1 100644 --- a/src/umleditor/mvc_model/entity.py +++ b/src/umleditor/mvc_model/entity.py @@ -47,23 +47,7 @@ def add_field(self, field_name:str): if field_name in self._fields: raise CustomExceptions.FieldExistsError(field_name) else: - self._fields.append(field_name) - - def edit_field(self, old_field: str, new_field: str): - """ - GUI specific command for editing fields - - Args: - old_field (str): The field' name to be removed - new_field (str): The field' name to be added - """ - if old_field == new_field: - return - else: - self.delete_field(old_field) - self.add_field(new_field) - - + self._fields.append(field_name) def delete_field(self, field_name: str): """ @@ -101,6 +85,8 @@ def rename_field(self, old_field: str, new_field: str): Returns: None. """ + if old_field == new_field: + return if old_field not in self._fields: raise CustomExceptions.FieldNotFoundError(old_field) diff --git a/src/umleditor/mvc_view/gui_view/class_card.py b/src/umleditor/mvc_view/gui_view/class_card.py index 908e7f9e..6c76b1ec 100644 --- a/src/umleditor/mvc_view/gui_view/class_card.py +++ b/src/umleditor/mvc_view/gui_view/class_card.py @@ -260,7 +260,7 @@ def verify_input(self, input: str, list: QListWidget): if self._old_text == "": self._process_task_signal.emit("fld -a " + self._class_label.text() + " " + input, self) else: - self._process_task_signal.emit("fld -e " + self._class_label.text() + " " + self._old_text + " " + input, self) + self._process_task_signal.emit("fld -r " + self._class_label.text() + " " + self._old_text + " " + input, self) elif list == self._list_method: # TODO given e.g. class add(one, two) run this command From 7070c6c34c2779bded93dc83d022ffe3af47f84f Mon Sep 17 00:00:00 2001 From: AdamG-L Date: Tue, 27 Feb 2024 18:05:06 -0500 Subject: [PATCH 072/144] Removing most debugging prints --- main.py | 4 ++-- src/umleditor/mvc_controller/gui_controller.py | 1 - src/umleditor/mvc_view/gui_view/class_card.py | 1 - src/umleditor/mvc_view/gui_view/view_GUI.py | 1 - 4 files changed, 2 insertions(+), 5 deletions(-) diff --git a/main.py b/main.py index d1676afb..365a955c 100644 --- a/main.py +++ b/main.py @@ -48,5 +48,5 @@ def mainGUI(): if not __debug__: debug_main() else: - main() - #mainGUI() + #main() + mainGUI() diff --git a/src/umleditor/mvc_controller/gui_controller.py b/src/umleditor/mvc_controller/gui_controller.py index 3aa94f8b..05a877fe 100644 --- a/src/umleditor/mvc_controller/gui_controller.py +++ b/src/umleditor/mvc_controller/gui_controller.py @@ -72,7 +72,6 @@ def add_field(self, widget): Parameters: widget: The widget instance. """ - print(widget.get_selected_line().text()) widget.get_selected_line().setReadOnly(True) widget.get_selected_line().setStyleSheet("background-color: white;") widget.enable_context_menus(True) diff --git a/src/umleditor/mvc_view/gui_view/class_card.py b/src/umleditor/mvc_view/gui_view/class_card.py index 6c76b1ec..de8fd6b4 100644 --- a/src/umleditor/mvc_view/gui_view/class_card.py +++ b/src/umleditor/mvc_view/gui_view/class_card.py @@ -255,7 +255,6 @@ def verify_input(self, input: str, list: QListWidget): input (str): The input text. widget (QWidget): The associated ClassCard widget. """ - print(input, self._old_text) if list == self._list_field: if self._old_text == "": self._process_task_signal.emit("fld -a " + self._class_label.text() + " " + input, self) diff --git a/src/umleditor/mvc_view/gui_view/view_GUI.py b/src/umleditor/mvc_view/gui_view/view_GUI.py index 00e097e5..cfe698f8 100644 --- a/src/umleditor/mvc_view/gui_view/view_GUI.py +++ b/src/umleditor/mvc_view/gui_view/view_GUI.py @@ -20,7 +20,6 @@ def __init__(self, *args, **kwargs): Initializes the ViewGUI instance and connect menu buttons """ super().__init__(*args, **kwargs) - print(os.path.dirname(__file__)) self._ui = uic.loadUi(os.path.join(os.path.dirname(__file__),"uml.ui"), self) self.connect_menu() self._x = 0 From 14710763709d1630430610e3784e8eeeaa4c9d07 Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 27 Feb 2024 18:42:57 -0500 Subject: [PATCH 073/144] Updated help menu to include Params and updated phrasings. Updated what a valid name includes. Updated valid entry in uml_parser doc string. --- src/umleditor/mvc_controller/uml_parser.py | 2 +- src/umleditor/mvc_model/help_command.py | 21 ++++++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/umleditor/mvc_controller/uml_parser.py b/src/umleditor/mvc_controller/uml_parser.py index 4076c6a0..874ca271 100644 --- a/src/umleditor/mvc_controller/uml_parser.py +++ b/src/umleditor/mvc_controller/uml_parser.py @@ -72,7 +72,7 @@ def __find_class(function:str): def check_args(args:list): '''Given a list of args, checks to make sure each one is valid. - Valid is defined as alphanumeric. + Valid includes alphanumeric, -, and _. Args: args(list): a list of strings to be checked diff --git a/src/umleditor/mvc_model/help_command.py b/src/umleditor/mvc_model/help_command.py index a158e4c1..f609a573 100644 --- a/src/umleditor/mvc_model/help_command.py +++ b/src/umleditor/mvc_model/help_command.py @@ -18,30 +18,33 @@ def help_menu(): "Below are the commands you can call and an explanation of what each does. Anything inside single quotes is decided\n" "by you! Enter the command, replacing anything in the single quotes, and the quotes themselves, with the name you\n" "want to use.\n\n" - "A valid name is made up of any combination of letters and numbers.\n\n" + "- A valid name is made up of any combination of alphnumeric characters, -, and _.\n" + "- Duplicates names are not allowed for an classes or attributes within a class.\n\n" #Class Commands "Class Commands:\n\t" - "class -a 'name' - adds a class with name 'name'. Cannot add classes with duplicate or invalid names\n\t" + "class -a 'name' - adds a class with name 'name'\n\t" "class -d 'name' - deletes a class with name 'name'\n\t" - "class -r 'old' 'new' - renames class 'old' to 'new'. Cannot rename classes to duplicate or invalid names\n" + "class -r 'old' 'new' - renames the class named 'old' to 'new'\n" #Field Commands "Field Commands: \n\t" - "fld -a 'class' 'name' - adds a field with name 'name' to class 'class'\n\t" - "fld -d 'class' 'name' - deletes a field with name 'name' from class 'class' if one exists\n\t" - "fld -r 'class' 'old' 'new' - renames a field from name 'old' to name 'new' in class 'class'\n" + "fld -a 'class' 'name' - adds a field named 'name' to the class named 'class'\n\t" + "fld -d 'class' 'name' - deletes a field named 'name' from the class named 'class'\n\t" + "fld -r 'class' 'old' 'new' - renames a field named 'old' to name 'new' in the class 'class'\n" #Method Commands "Method Commands:\n\t" "mthd -a 'class' 'name' - adds a method with the name 'name' to the class 'class'\n\t" - "mthd -d 'class' 'name' - deletes a method with name 'name' from class 'class' if one exists\n\t" + "mthd -d 'class' 'name' - deletes a method with name 'name' from class 'class'\n\t" "mthd -r 'class' 'old 'new' - reames a method form name 'old' to name 'new' in class 'class'\n" #Paramater Commands "Parameter Commands:\n\t" - "???\n" + "prm -a 'class' 'mthd' 'prm' - adds one or more params to a method (seperate each with a space)\n\t" + "prm -d 'class' 'mthd' 'prm' - deletes the param from the method if it exists\n\t" + "prm -c 'class' 'mthd' 'prm' - replaces the params in the method with the new param(s) supplied\n" #Relation Commands "Relation Commands:\n\t" "rel -a 'src' 'dest' 'type' - adds a relationship between class 'src' and class 'dest' of type 'type'\n\t" "rel -t 'src' 'dest' 'type' - changes the type of the relationship between class 'src' and class 'dest' to 'new type'\n\t" - "rel -d 'src' 'dest' - deletes a relationship between class 'src' and class 'dest' if one exists\n" + "rel -d 'src' 'dest' - deletes a relationship between class 'src' and class 'dest'\n" #List Commands "List Flags: \n\t" "list -a - list all classes and their contents in the UML Diagram\n\t" From 7f66b9b2cc2522a7863f51cbae860b4971bbc3d5 Mon Sep 17 00:00:00 2001 From: AdamG-L Date: Tue, 27 Feb 2024 18:55:16 -0500 Subject: [PATCH 074/144] Add relation functionality --- .../mvc_controller/gui_controller.py | 6 ++-- src/umleditor/mvc_view/gui_view/class_card.py | 34 +++++++++---------- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/src/umleditor/mvc_controller/gui_controller.py b/src/umleditor/mvc_controller/gui_controller.py index 05a877fe..ca443917 100644 --- a/src/umleditor/mvc_controller/gui_controller.py +++ b/src/umleditor/mvc_controller/gui_controller.py @@ -50,8 +50,8 @@ def run(self, task: str, widget: QtWidgets): # Successful task if "class -a" in task: self.add_class(task, widget) - elif "fld -a" in task or "fld -r" in task: - self.add_field(widget) + elif "fld -a" in task or "fld -r" in task or "rel -a" in task: + self.acceptance_state(widget) def add_class(self, task: str, widget: QtWidgets): """ @@ -65,7 +65,7 @@ def add_class(self, task: str, widget: QtWidgets): entity_name = task.split()[-1] self._window.add_class_card(entity_name) - def add_field(self, widget): + def acceptance_state(self, widget): """ Makes text read-only and returns diagram to original state diff --git a/src/umleditor/mvc_view/gui_view/class_card.py b/src/umleditor/mvc_view/gui_view/class_card.py index de8fd6b4..0e967712 100644 --- a/src/umleditor/mvc_view/gui_view/class_card.py +++ b/src/umleditor/mvc_view/gui_view/class_card.py @@ -100,16 +100,16 @@ def show_class_menu(self, position): menu = QMenu() field_action = QAction("Add Field", self) # TODO method_action = QAction("Add Method", self) - # TODO relation_action = QAction("Add Relation", self) + relation_action = QAction("Add Relation", self) menu.addAction(field_action) # TODO menu.addAction(method_action) - # TODO menu.addAction(relation_action) + menu.addAction(relation_action) # Add button functionality field_action.triggered.connect(lambda: self.menu_action_clicked(self._list_field, "Enter Field")) # TODO method_action.triggered.connect(lambda: self.menu_action_clicked(self._list_method, "e.g. add(int, int)")) - # TODO relation_action.triggered.connect(lambda: self.menu_action_clicked(self._list_field, "Enter Field")) + relation_action.triggered.connect(lambda: self.menu_action_clicked(self._list_relation, "e.g. src dst type")) # Create Menu menu.exec(self.mapToGlobal(position)) @@ -237,17 +237,7 @@ def enable_context_menus(self, enable: bool): if isinstance(current_widget, QWidget): stack.extend(current_widget.findChildren(QWidget)) - - def enable_all_items(self): - """ - Enables context menus for all items within the ClassCard - """ - self._class_label.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) - self._list_field.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) - self._list_method.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) - self._list_relation.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) - - def verify_input(self, input: str, list: QListWidget): + def verify_input(self, new_text: str, list: QListWidget): """ Sends a signal for the task to be processed. @@ -257,13 +247,21 @@ def verify_input(self, input: str, list: QListWidget): """ if list == self._list_field: if self._old_text == "": - self._process_task_signal.emit("fld -a " + self._class_label.text() + " " + input, self) + self._process_task_signal.emit("fld -a " + self._class_label.text() + " " + new_text, self) else: - self._process_task_signal.emit("fld -r " + self._class_label.text() + " " + self._old_text + " " + input, self) + self._process_task_signal.emit("fld -r " + self._class_label.text() + " " + self._old_text + " " + new_text, self) - elif list == self._list_method: - # TODO given e.g. class add(one, two) run this command + elif list == self._list_relation: + if self._old_text == "": + words = self.split_relation(new_text) + self._process_task_signal.emit("rel -a " + words[0] + " " + words[1] + " " + words[2], self) pass + + def split_relation(self, text: str): + words = text.split() + while len(words) < 3: + words.append("") + return words def get_selected_line(self): """ From 9d46b711e66b92fc7e2b7379266c3b0e365db668 Mon Sep 17 00:00:00 2001 From: AdamG-L Date: Tue, 27 Feb 2024 18:57:18 -0500 Subject: [PATCH 075/144] Fixed Issue with relation allowing more than one type at a time --- src/umleditor/mvc_model/relation.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/umleditor/mvc_model/relation.py b/src/umleditor/mvc_model/relation.py index 7116d311..e8d1939a 100644 --- a/src/umleditor/mvc_model/relation.py +++ b/src/umleditor/mvc_model/relation.py @@ -108,7 +108,4 @@ def __eq__(self, other): if self._destination != other._destination: return False - if self._type != other._type: - return False - return True \ No newline at end of file From 428abffda99cdf245508e83bb62da28444c7ef73 Mon Sep 17 00:00:00 2001 From: AdamG-L Date: Tue, 27 Feb 2024 20:22:41 -0500 Subject: [PATCH 076/144] Working edit command for relations --- src/umleditor/mvc_controller/gui_controller.py | 5 ++++- src/umleditor/mvc_controller/uml_lexer.py | 4 ++-- src/umleditor/mvc_model/diagram.py | 18 ++++++++++++++++++ src/umleditor/mvc_view/gui_view/class_card.py | 8 ++++++-- 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/umleditor/mvc_controller/gui_controller.py b/src/umleditor/mvc_controller/gui_controller.py index ca443917..99bf6969 100644 --- a/src/umleditor/mvc_controller/gui_controller.py +++ b/src/umleditor/mvc_controller/gui_controller.py @@ -50,7 +50,10 @@ def run(self, task: str, widget: QtWidgets): # Successful task if "class -a" in task: self.add_class(task, widget) - elif "fld -a" in task or "fld -r" in task or "rel -a" in task: + # No action required after deleting + elif "-d" in task: + return + else: self.acceptance_state(widget) def add_class(self, task: str, widget: QtWidgets): diff --git a/src/umleditor/mvc_controller/uml_lexer.py b/src/umleditor/mvc_controller/uml_lexer.py index 80c18ca6..2f50d9a7 100644 --- a/src/umleditor/mvc_controller/uml_lexer.py +++ b/src/umleditor/mvc_controller/uml_lexer.py @@ -7,7 +7,7 @@ "list" : ["a","c","r","d"], "fld" : ["a","d","r"], "mthd" : ["a","d","r"], - "rel" : ["a","t","d"], + "rel" : ["a","t","d","e"], "save" : [""], "load" : [""], "exit" : [""], @@ -22,7 +22,7 @@ "list" : ["list_everything","list_entities","list_relations","list_entity_details"], "fld" : ["add_field","delete_field","rename_field"], "mthd" : ["add_method","delete_method","rename_method"], - "rel" : ["add_relation","change_relation_type","delete_relation"], + "rel" : ["add_relation","change_relation_type","delete_relation","edit_relation"], "save" : ["save"], "load" : ["load"], "exit" : ["quit"], diff --git a/src/umleditor/mvc_model/diagram.py b/src/umleditor/mvc_model/diagram.py index a2c93b98..0717409a 100644 --- a/src/umleditor/mvc_model/diagram.py +++ b/src/umleditor/mvc_model/diagram.py @@ -266,3 +266,21 @@ def change_relation_type(self, source:str, destination:str, new_type:str): return raise CustomExceptions.RelationDoesNotExistError(source, destination) + def edit_relation(self, old_src: str, old_dst: str, old_type: str, + new_src:str, new_dst:str, new_type:str): + src = self.get_entity(new_src) + dst = self.get_entity(new_dst) + # If input unchanged, return + if old_src == new_src and old_dst == new_dst and old_type == new_type: + return + # If src, dst same attempt to change relation type + elif old_src == new_src and old_dst == new_dst: + self.change_relation_type(new_src, new_dst, new_type) + # Otherwise new relation T.F. add then delete + else: + self.add_relation(new_src, new_dst, new_type) + self.delete_relation(old_src, old_dst) + + + + diff --git a/src/umleditor/mvc_view/gui_view/class_card.py b/src/umleditor/mvc_view/gui_view/class_card.py index 0e967712..dfcd5a8c 100644 --- a/src/umleditor/mvc_view/gui_view/class_card.py +++ b/src/umleditor/mvc_view/gui_view/class_card.py @@ -131,8 +131,11 @@ def show_row_menu(self, position, widget: QLineEdit): edit_action.triggered.connect(lambda: self.edit_action_clicked(widget)) delete_action.triggered.connect(lambda: self.delete_action_clicked(widget)) + # Map the position to global coordinates + global_position = widget.mapToGlobal(position) + # Create Menu - menu.exec(self.mapToGlobal(position)) + menu.exec(global_position) def edit_action_clicked(self, widget: QLineEdit): """ @@ -255,7 +258,8 @@ def verify_input(self, new_text: str, list: QListWidget): if self._old_text == "": words = self.split_relation(new_text) self._process_task_signal.emit("rel -a " + words[0] + " " + words[1] + " " + words[2], self) - pass + else: + self._process_task_signal.emit("rel -e " + self._old_text + " " + new_text, self) def split_relation(self, text: str): words = text.split() From d1078c45f8d1ba30d1457f152be3a529a710a64e Mon Sep 17 00:00:00 2001 From: AdamG-L Date: Tue, 27 Feb 2024 21:03:39 -0500 Subject: [PATCH 077/144] Escape key now deletes active row --- .../mvc_controller/gui_controller.py | 1 + src/umleditor/mvc_view/gui_view/class_card.py | 45 +++++++++++++------ 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/umleditor/mvc_controller/gui_controller.py b/src/umleditor/mvc_controller/gui_controller.py index 99bf6969..33672e16 100644 --- a/src/umleditor/mvc_controller/gui_controller.py +++ b/src/umleditor/mvc_controller/gui_controller.py @@ -78,5 +78,6 @@ def acceptance_state(self, widget): widget.get_selected_line().setReadOnly(True) widget.get_selected_line().setStyleSheet("background-color: white;") widget.enable_context_menus(True) + widget.deselect_line() self._window.enable_widgets(True, self) diff --git a/src/umleditor/mvc_view/gui_view/class_card.py b/src/umleditor/mvc_view/gui_view/class_card.py index dfcd5a8c..cdf1b81a 100644 --- a/src/umleditor/mvc_view/gui_view/class_card.py +++ b/src/umleditor/mvc_view/gui_view/class_card.py @@ -1,6 +1,6 @@ from PyQt6.QtWidgets import QWidget, QVBoxLayout, QListWidget, QMenu, QLineEdit, QLabel, QListWidgetItem from PyQt6.QtGui import QAction -from PyQt6.QtCore import Qt, pyqtSignal +from PyQt6.QtCore import Qt, pyqtSignal, QEvent class ClassCard(QWidget): """ @@ -23,6 +23,8 @@ def __init__(self, name: str): super().__init__() self.set_name(name) self.initUI() + # Used for capturing escape key + self.installEventFilter(self) def set_name(self, name: str): """ @@ -167,6 +169,10 @@ def delete_action_clicked(self, widget: QLineEdit): # Delete field from diagram self._process_task_signal.emit("fld -d " + self._class_label.text() + " " + widget.text(), self) + # Enable widgets/menus + self._enable_widgets_signal.emit(True, self) + self.enable_context_menus(True) + lists = [(self._list_field, self._list_field.count()), (self._list_relation, self._list_relation.count()), (self._list_method, self._list_method.count())] @@ -180,18 +186,6 @@ def delete_action_clicked(self, widget: QLineEdit): list_widget.removeItemWidget(item) list_widget.takeItem(index) return - - - - - def unselected_state(self): - """ - Returns the Class Card and all widgets to an unselected state - """ - self._enable_widgets_signal.emit(True, self) - self.enable_context_menus(True) - self._selected_line.setReadOnly(True) - self._selected_line.setStyleSheet("background-color: white;") def menu_action_clicked(self, list: QListWidget, placeholder: str): @@ -225,6 +219,15 @@ def menu_action_clicked(self, list: QListWidget, placeholder: str): # Disable all context menus while actively editing self.enable_context_menus(False) + + def eventFilter(self, obj, event: QEvent): + if event.type() == QEvent.Type.KeyPress: + if event.key() == Qt.Key.Key_Escape: + # Handle the escape key press here + if self._selected_line != None: + self.delete_action_clicked(self._selected_line) + return True + return super().eventFilter(obj, event) def enable_context_menus(self, enable: bool): """ @@ -262,11 +265,27 @@ def verify_input(self, new_text: str, list: QListWidget): self._process_task_signal.emit("rel -e " + self._old_text + " " + new_text, self) def split_relation(self, text: str): + """ + Splits a string into three words. + + Args: + text (str): The input string to be split. + + Returns: + list: A list containing three words. If the input string has fewer than three words, + the remaining elements in the list will be empty strings. + """ words = text.split() while len(words) < 3: words.append("") return words + def deselect_line(self): + """ + Deselects the currently selected QLineEdit. + """ + self._selected_line = None + def get_selected_line(self): """ Return the currently selected line. From 45f40b807c77fa7a2b4b467dd9913de7fa0f2479 Mon Sep 17 00:00:00 2001 From: AdamG-L Date: Tue, 27 Feb 2024 21:15:00 -0500 Subject: [PATCH 078/144] Adding specialized deleting --- src/umleditor/mvc_view/gui_view/class_card.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/umleditor/mvc_view/gui_view/class_card.py b/src/umleditor/mvc_view/gui_view/class_card.py index cdf1b81a..70bf1035 100644 --- a/src/umleditor/mvc_view/gui_view/class_card.py +++ b/src/umleditor/mvc_view/gui_view/class_card.py @@ -167,7 +167,7 @@ def delete_action_clicked(self, widget: QLineEdit): widget (QLineEdit): The QLineEdit widget to be removed. """ # Delete field from diagram - self._process_task_signal.emit("fld -d " + self._class_label.text() + " " + widget.text(), self) + #self._process_task_signal.emit("fld -d " + self._class_label.text() + " " + widget.text(), self) # Enable widgets/menus self._enable_widgets_signal.emit(True, self) @@ -183,6 +183,12 @@ def delete_action_clicked(self, widget: QLineEdit): if item is not None: line_edit = list_widget.itemWidget(item) if line_edit == widget: + # Call specific delete based on list field + if list_widget is self._list_field: + self._process_task_signal.emit("fld -d " + self._class_label.text() + " " + widget.text(), self) + elif list_widget is self._list_relation: + relation = widget.text().split() + self._process_task_signal.emit("rel -d " + relation[0] + " " + relation[1], self) list_widget.removeItemWidget(item) list_widget.takeItem(index) return From 355eba30c7d65e3d371f70a0f077b4fee7614a10 Mon Sep 17 00:00:00 2001 From: AdamG-L Date: Tue, 27 Feb 2024 21:33:43 -0500 Subject: [PATCH 079/144] Working add and edit relation --- src/umleditor/mvc_view/gui_view/class_card.py | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/umleditor/mvc_view/gui_view/class_card.py b/src/umleditor/mvc_view/gui_view/class_card.py index 70bf1035..f3eb7460 100644 --- a/src/umleditor/mvc_view/gui_view/class_card.py +++ b/src/umleditor/mvc_view/gui_view/class_card.py @@ -111,7 +111,7 @@ def show_class_menu(self, position): # Add button functionality field_action.triggered.connect(lambda: self.menu_action_clicked(self._list_field, "Enter Field")) # TODO method_action.triggered.connect(lambda: self.menu_action_clicked(self._list_method, "e.g. add(int, int)")) - relation_action.triggered.connect(lambda: self.menu_action_clicked(self._list_relation, "e.g. src dst type")) + relation_action.triggered.connect(lambda: self.menu_action_clicked(self._list_relation, "e.g. dst type")) # Create Menu menu.exec(self.mapToGlobal(position)) @@ -173,6 +173,8 @@ def delete_action_clicked(self, widget: QLineEdit): self._enable_widgets_signal.emit(True, self) self.enable_context_menus(True) + class_name = self._class_label.text() + lists = [(self._list_field, self._list_field.count()), (self._list_relation, self._list_relation.count()), (self._list_method, self._list_method.count())] @@ -185,10 +187,10 @@ def delete_action_clicked(self, widget: QLineEdit): if line_edit == widget: # Call specific delete based on list field if list_widget is self._list_field: - self._process_task_signal.emit("fld -d " + self._class_label.text() + " " + widget.text(), self) + self._process_task_signal.emit("fld -d " + class_name + " " + widget.text(), self) elif list_widget is self._list_relation: relation = widget.text().split() - self._process_task_signal.emit("rel -d " + relation[0] + " " + relation[1], self) + self._process_task_signal.emit("rel -d " + class_name + " " + relation[0], self) list_widget.removeItemWidget(item) list_widget.takeItem(index) return @@ -257,32 +259,36 @@ def verify_input(self, new_text: str, list: QListWidget): input (str): The input text. widget (QWidget): The associated ClassCard widget. """ + class_name = self._class_label.text() + # Field task signals if list == self._list_field: if self._old_text == "": - self._process_task_signal.emit("fld -a " + self._class_label.text() + " " + new_text, self) + self._process_task_signal.emit("fld -a " + class_name + " " + new_text, self) else: - self._process_task_signal.emit("fld -r " + self._class_label.text() + " " + self._old_text + " " + new_text, self) - + self._process_task_signal.emit("fld -r " + class_name + " " + + self._old_text + " " + new_text, self) + # Relation task signals - (Source, Destination, Type) elif list == self._list_relation: if self._old_text == "": words = self.split_relation(new_text) - self._process_task_signal.emit("rel -a " + words[0] + " " + words[1] + " " + words[2], self) + self._process_task_signal.emit("rel -a " + class_name + " " + " ".join(words), self) else: - self._process_task_signal.emit("rel -e " + self._old_text + " " + new_text, self) + self._process_task_signal.emit("rel -e " + class_name + " " + self._old_text + " " + + class_name + " " + new_text, self) def split_relation(self, text: str): """ - Splits a string into three words. + Splits a string into two words. Args: text (str): The input string to be split. Returns: - list: A list containing three words. If the input string has fewer than three words, + list: A list containing three words. If the input string has fewer than two words, the remaining elements in the list will be empty strings. """ words = text.split() - while len(words) < 3: + while len(words) < 2: words.append("") return words From c68d0ffc94e6aca658438fb778e1936e037a1f6e Mon Sep 17 00:00:00 2001 From: AdamG-L Date: Tue, 27 Feb 2024 22:03:38 -0500 Subject: [PATCH 080/144] Escape functionality Debugged --- src/umleditor/mvc_controller/gui_controller.py | 4 ++-- src/umleditor/mvc_view/gui_view/class_card.py | 15 ++++++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/umleditor/mvc_controller/gui_controller.py b/src/umleditor/mvc_controller/gui_controller.py index 33672e16..331a012e 100644 --- a/src/umleditor/mvc_controller/gui_controller.py +++ b/src/umleditor/mvc_controller/gui_controller.py @@ -43,8 +43,8 @@ def run(self, task: str, widget: QtWidgets): out = super().run(task) except Exception as e: # Ignore attempting to delete things that don't exist - if isinstance(e, CE.FieldNotFoundError): - pass + #if isinstance(e, CE.FieldNotFoundError) or isinstance(e, CE.RelationDoesNotExistError): + # return self._window.invalid_input_message(str(e)) return # Successful task diff --git a/src/umleditor/mvc_view/gui_view/class_card.py b/src/umleditor/mvc_view/gui_view/class_card.py index f3eb7460..2c299969 100644 --- a/src/umleditor/mvc_view/gui_view/class_card.py +++ b/src/umleditor/mvc_view/gui_view/class_card.py @@ -129,6 +129,9 @@ def show_row_menu(self, position, widget: QLineEdit): menu.addAction(edit_action) menu.addAction(delete_action) + # Save current text within row + self._old_text = widget.text() + # Add button functionality edit_action.triggered.connect(lambda: self.edit_action_clicked(widget)) delete_action.triggered.connect(lambda: self.delete_action_clicked(widget)) @@ -166,8 +169,10 @@ def delete_action_clicked(self, widget: QLineEdit): Args: widget (QLineEdit): The QLineEdit widget to be removed. """ - # Delete field from diagram - #self._process_task_signal.emit("fld -d " + self._class_label.text() + " " + widget.text(), self) + # Boolean for whether diagram is also updated + is_new_row = False + if self._old_text == "": + is_new_row = True # Enable widgets/menus self._enable_widgets_signal.emit(True, self) @@ -186,10 +191,10 @@ def delete_action_clicked(self, widget: QLineEdit): line_edit = list_widget.itemWidget(item) if line_edit == widget: # Call specific delete based on list field - if list_widget is self._list_field: + if list_widget is self._list_field and not is_new_row: self._process_task_signal.emit("fld -d " + class_name + " " + widget.text(), self) - elif list_widget is self._list_relation: - relation = widget.text().split() + elif list_widget is self._list_relation and not is_new_row: + relation = self.split_relation(widget.text()) self._process_task_signal.emit("rel -d " + class_name + " " + relation[0], self) list_widget.removeItemWidget(item) list_widget.takeItem(index) From 5c2c9a538e5cc52e501b60382b8aaa723d5675b9 Mon Sep 17 00:00:00 2001 From: AdamG-L Date: Tue, 27 Feb 2024 22:04:46 -0500 Subject: [PATCH 081/144] Commented out mainGUI() --- main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index 365a955c..d1676afb 100644 --- a/main.py +++ b/main.py @@ -48,5 +48,5 @@ def mainGUI(): if not __debug__: debug_main() else: - #main() - mainGUI() + main() + #mainGUI() From c898fb15f3ae5e984b4ae058fc2dbee1db86d8f9 Mon Sep 17 00:00:00 2001 From: AdamG-L Date: Tue, 27 Feb 2024 22:09:41 -0500 Subject: [PATCH 082/144] Added comments to methods --- src/umleditor/mvc_model/diagram.py | 12 ++++++++++++ src/umleditor/mvc_view/gui_view/class_card.py | 10 ++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/umleditor/mvc_model/diagram.py b/src/umleditor/mvc_model/diagram.py index 0717409a..7f8e4c26 100644 --- a/src/umleditor/mvc_model/diagram.py +++ b/src/umleditor/mvc_model/diagram.py @@ -268,6 +268,18 @@ def change_relation_type(self, source:str, destination:str, new_type:str): def edit_relation(self, old_src: str, old_dst: str, old_type: str, new_src:str, new_dst:str, new_type:str): + """ + This method allows for editing an existing relation between two entities by modifying their source, + destination, or type. If the input parameters remain unchanged, no action is taken. + + Args: + old_src (str): The original source entity. + old_dst (str): The original destination entity. + old_type (str): The original type of relation. + new_src (str): The new source entity. + new_dst (str): The new destination entity. + new_type (str): The new type of relation. + """ src = self.get_entity(new_src) dst = self.get_entity(new_dst) # If input unchanged, return diff --git a/src/umleditor/mvc_view/gui_view/class_card.py b/src/umleditor/mvc_view/gui_view/class_card.py index 2c299969..3445137b 100644 --- a/src/umleditor/mvc_view/gui_view/class_card.py +++ b/src/umleditor/mvc_view/gui_view/class_card.py @@ -234,6 +234,16 @@ def menu_action_clicked(self, list: QListWidget, placeholder: str): self.enable_context_menus(False) def eventFilter(self, obj, event: QEvent): + """ + Captures escape key inputs and deletes a row if a selected row exists + + Args: + obj: The object for which events are being filtered. + event (QEvent): The event to be filtered. + + Returns: + bool: True if the event has been handled, False otherwise. + """ if event.type() == QEvent.Type.KeyPress: if event.key() == Qt.Key.Key_Escape: # Handle the escape key press here From 5c95e67bccc84508a57e8b68f9d5cfd66ea45d65 Mon Sep 17 00:00:00 2001 From: AdamG-L Date: Wed, 28 Feb 2024 16:04:33 -0500 Subject: [PATCH 083/144] Restructured Escape functionality to either delete a new row or return an existing row to original state --- main.py | 4 +- src/umleditor/mvc_view/gui_view/class_card.py | 52 ++++++++++++++----- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/main.py b/main.py index d1676afb..365a955c 100644 --- a/main.py +++ b/main.py @@ -48,5 +48,5 @@ def mainGUI(): if not __debug__: debug_main() else: - main() - #mainGUI() + #main() + mainGUI() diff --git a/src/umleditor/mvc_view/gui_view/class_card.py b/src/umleditor/mvc_view/gui_view/class_card.py index 3445137b..8bf8c359 100644 --- a/src/umleditor/mvc_view/gui_view/class_card.py +++ b/src/umleditor/mvc_view/gui_view/class_card.py @@ -169,15 +169,6 @@ def delete_action_clicked(self, widget: QLineEdit): Args: widget (QLineEdit): The QLineEdit widget to be removed. """ - # Boolean for whether diagram is also updated - is_new_row = False - if self._old_text == "": - is_new_row = True - - # Enable widgets/menus - self._enable_widgets_signal.emit(True, self) - self.enable_context_menus(True) - class_name = self._class_label.text() lists = [(self._list_field, self._list_field.count()), @@ -191,14 +182,17 @@ def delete_action_clicked(self, widget: QLineEdit): line_edit = list_widget.itemWidget(item) if line_edit == widget: # Call specific delete based on list field - if list_widget is self._list_field and not is_new_row: + if list_widget is self._list_field: self._process_task_signal.emit("fld -d " + class_name + " " + widget.text(), self) - elif list_widget is self._list_relation and not is_new_row: + elif list_widget is self._list_relation: relation = self.split_relation(widget.text()) self._process_task_signal.emit("rel -d " + class_name + " " + relation[0], self) list_widget.removeItemWidget(item) list_widget.takeItem(index) return + # Enable widgets/menus + self._enable_widgets_signal.emit(True, self) + self.enable_context_menus(True) def menu_action_clicked(self, list: QListWidget, placeholder: str): @@ -248,10 +242,44 @@ def eventFilter(self, obj, event: QEvent): if event.key() == Qt.Key.Key_Escape: # Handle the escape key press here if self._selected_line != None: - self.delete_action_clicked(self._selected_line) + self.escape_from_row() return True return super().eventFilter(obj, event) + def escape_from_row(self): + """ + Method to escape from row editing mode. If it's a newly added row, removes the row from the class card. + Otherwise, returns the row to its original state. + """ + lists = [(self._list_field, self._list_field.count()), + (self._list_relation, self._list_relation.count()), + (self._list_method, self._list_method.count())] + + # If newly added row, just remove from class card + if self._old_text == "": + for list_widget, count in lists: + for index in range(count): + item = list_widget.item(index) + if item is not None: + line_edit = list_widget.itemWidget(item) + if line_edit == self._selected_line: + # Call specific delete based on list field + list_widget.removeItemWidget(item) + list_widget.takeItem(index) + break + # Otherwise return the row to it's original state + else: + self._selected_line.setText(self._old_text) + + # Return to unselected state + self._enable_widgets_signal.emit(True, self) + self.enable_context_menus(True) + self._selected_line.setReadOnly(True) + self._selected_line.setStyleSheet("background-color: white;") + + + + def enable_context_menus(self, enable: bool): """ Enable/disable context menus for all items within the ClassCard From c569b84761895797f1194a0af99e4124bd82a533 Mon Sep 17 00:00:00 2001 From: AdamG-L Date: Wed, 28 Feb 2024 16:31:19 -0500 Subject: [PATCH 084/144] Making add method menu --- src/umleditor/mvc_view/gui_view/class_card.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/umleditor/mvc_view/gui_view/class_card.py b/src/umleditor/mvc_view/gui_view/class_card.py index 8bf8c359..367578c7 100644 --- a/src/umleditor/mvc_view/gui_view/class_card.py +++ b/src/umleditor/mvc_view/gui_view/class_card.py @@ -101,16 +101,16 @@ def show_class_menu(self, position): # Create menu & Actions menu = QMenu() field_action = QAction("Add Field", self) - # TODO method_action = QAction("Add Method", self) + method_action = QAction("Add Method", self) relation_action = QAction("Add Relation", self) menu.addAction(field_action) - # TODO menu.addAction(method_action) + menu.addAction(method_action) menu.addAction(relation_action) # Add button functionality field_action.triggered.connect(lambda: self.menu_action_clicked(self._list_field, "Enter Field")) - # TODO method_action.triggered.connect(lambda: self.menu_action_clicked(self._list_method, "e.g. add(int, int)")) + method_action.triggered.connect(lambda: self.menu_action_clicked(self._list_method, "e.g. method param1 param2...")) relation_action.triggered.connect(lambda: self.menu_action_clicked(self._list_relation, "e.g. dst type")) # Create Menu menu.exec(self.mapToGlobal(position)) @@ -229,7 +229,7 @@ def menu_action_clicked(self, list: QListWidget, placeholder: str): def eventFilter(self, obj, event: QEvent): """ - Captures escape key inputs and deletes a row if a selected row exists + Captures escape key inputs and escapes from a selected row Args: obj: The object for which events are being filtered. @@ -318,6 +318,10 @@ def verify_input(self, new_text: str, list: QListWidget): else: self._process_task_signal.emit("rel -e " + class_name + " " + self._old_text + " " + class_name + " " + new_text, self) + # Method task signals - methodName param1 param2 + else: + pass + def split_relation(self, text: str): """ From ffeac15fa170303b5534229b4b131049dd8dbe17 Mon Sep 17 00:00:00 2001 From: AdamG-L Date: Wed, 28 Feb 2024 19:11:04 -0500 Subject: [PATCH 085/144] Working widow dimensions, class card spawning, and max size --- src/umleditor/mvc_view/gui_view/uml.ui | 35 ++++++++++++--------- src/umleditor/mvc_view/gui_view/view_GUI.py | 27 +++++++++++++--- 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/src/umleditor/mvc_view/gui_view/uml.ui b/src/umleditor/mvc_view/gui_view/uml.ui index d9794d3e..a4888a87 100644 --- a/src/umleditor/mvc_view/gui_view/uml.ui +++ b/src/umleditor/mvc_view/gui_view/uml.ui @@ -6,32 +6,39 @@ 0 0 - 800 - 600 + 850 + 850 + + + 850 + 850 + + UML Editor - - - - -1 - -1 - 801 - 571 - - - - + + + 0 + 0 + + + + + 0 + 0 + + 0 0 - 800 + 850 22 diff --git a/src/umleditor/mvc_view/gui_view/view_GUI.py b/src/umleditor/mvc_view/gui_view/view_GUI.py index cfe698f8..80e7ce3c 100644 --- a/src/umleditor/mvc_view/gui_view/view_GUI.py +++ b/src/umleditor/mvc_view/gui_view/view_GUI.py @@ -1,7 +1,7 @@ import os from PyQt6 import QtCore, QtGui, QtWidgets from PyQt6 import uic -from PyQt6.QtWidgets import QMessageBox, QWidget, QVBoxLayout, QMenuBar, QLineEdit +from PyQt6.QtWidgets import QMessageBox, QWidget, QMenuBar, QGridLayout from PyQt6.QtCore import pyqtSignal from umleditor.mvc_view.gui_view.class_input_dialog import ClassInputDialog from umleditor.mvc_view.gui_view.class_card import ClassCard @@ -22,7 +22,13 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._ui = uic.loadUi(os.path.join(os.path.dirname(__file__),"uml.ui"), self) self.connect_menu() - self._x = 0 + # Create grid and max sizes + self._grid_layout = QGridLayout(self._ui.centralwidget) + self._max_size = 20 + self._max_column = 5 + self._size = 0 + self._row = 0 + self._column = 0 def get_signal(self): """ @@ -71,6 +77,9 @@ def confirm_class_clicked(self): """ On Confirm emits signal to process task """ + if self._size >= self._max_size: + self.invalid_input_message("No more than " + str(self._max_size) + " Class Cards in a single diagram!") + return task = 'class -a ' + self._dialog.input_text.text() # Emit signal to controller to handle task self._process_task_signal.emit(task, self._dialog) @@ -86,8 +95,18 @@ def add_class_card(self, name: str): class_card = ClassCard(name) class_card.get_task_signal().connect(self.forward_signal) class_card.get_enable_signal().connect(self.enable_widgets) - self._ui.gridLayout.addWidget(class_card, self._x, self._x) - self._x = self._x + 1 + if self._size == 0: + self._grid_layout.addWidget(class_card, self._row, self._column) + self._size += 1 + else: + if self._column < self._max_column - 1: + self._column += 1 + else: + self._row += 1 + self._column = 0 + self._size += 1 + self._grid_layout.addWidget(class_card, self._row, self._column) + def enable_widgets(self, enabled: bool, active_widget: QWidget): """ From ab7d33f2ff7c5959636d4272e5a60a3185cacdc3 Mon Sep 17 00:00:00 2001 From: AdamG-L Date: Wed, 28 Feb 2024 19:12:19 -0500 Subject: [PATCH 086/144] comment out main --- main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index 365a955c..d1676afb 100644 --- a/main.py +++ b/main.py @@ -48,5 +48,5 @@ def mainGUI(): if not __debug__: debug_main() else: - #main() - mainGUI() + main() + #mainGUI() From b689e1596c420a1134a98b94078e4abb0d52a768 Mon Sep 17 00:00:00 2001 From: Peter F Date: Wed, 28 Feb 2024 19:28:20 -0500 Subject: [PATCH 087/144] NOT WORKING --- src/umleditor/mvc_controller/controller.py | 2 +- src/umleditor/mvc_controller/uml_parser.py | 7 ++---- src/umleditor/mvc_model/entity.py | 26 +++++++++++++++++++--- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/umleditor/mvc_controller/controller.py b/src/umleditor/mvc_controller/controller.py index 9df53a52..7bc1a49b 100644 --- a/src/umleditor/mvc_controller/controller.py +++ b/src/umleditor/mvc_controller/controller.py @@ -29,7 +29,7 @@ def run(self, line:str) -> str: except TypeError as t: raise CE.InvalidArgCountError(t) except ValueError as v: - print(str(v)) + raise CE.NeedsMoreInput() except Exception as e: raise e diff --git a/src/umleditor/mvc_controller/uml_parser.py b/src/umleditor/mvc_controller/uml_parser.py index 1693123c..0fd0e32e 100644 --- a/src/umleditor/mvc_controller/uml_parser.py +++ b/src/umleditor/mvc_controller/uml_parser.py @@ -37,16 +37,13 @@ def parse (c, input:str) -> list: #UML_Method has enough extra work that needs to be done for it that it's just its own case. if command_class == UML_Method: bits = bits[2:] - print("in UML_Method") #get the object and prep the list for splitting ent = c._diagram.get_entity(bits.pop(0)) obj = ent.get_method(bits.pop(0)) - print("before split") - args = __split_list(bits[2:]) - print("past split") + args = __split_list(bits) for arg in args: check_args(arg) - print("above return") + print("before return") return [getattr(obj, command_str)] + args #if the args aren't a list, check them as normal diff --git a/src/umleditor/mvc_model/entity.py b/src/umleditor/mvc_model/entity.py index 4688ebde..17320e0b 100644 --- a/src/umleditor/mvc_model/entity.py +++ b/src/umleditor/mvc_model/entity.py @@ -208,7 +208,7 @@ def list_methods(self): Return: a comma separated list of all methods and their params in this entity ''' - return ", ".join(str(m) for m in self._methods) + '\n' + return ", ".join(m.__str__() for m in self._methods) + '\n' def __str__(self) -> str: """ @@ -228,7 +228,12 @@ def __eq__ (self, other): ''' return self._name == other._name +#====================================================================================# +# Method Definition +#====================================================================================# + class UML_Method: + def __init__(self, method_name=''): """ Creates a UML_Method object. @@ -289,9 +294,10 @@ def add_parameters(self, params: list[str]): None. """ for new_param in params: + print("outside if") if new_param in self._params: raise CustomExceptions.ParameterExistsError(new_param) - self._params.extend(params) + self._params.append(new_param) def remove_parameters(self, params: list[str]): """ @@ -310,7 +316,7 @@ def remove_parameters(self, params: list[str]): if remove_param not in self._params: raise CustomExceptions.ParameterNotFoundError(remove_param) for remove_param in params: - del self._params[self._params.index(remove_param)] + self._params.remove(remove_param) def change_parameters(self, old_params: list[str], new_params: list[str]): """ @@ -361,3 +367,17 @@ def __str__(self): result += "\n\t" + self.get_method_name() + "'s Params:\n\t\t" param_results = ', '.join(p for p in self._params) return result + param_results + + def __eq__ (self, o): + """Equality operator for Methods - checks equality of both fields""" + if self is o: + return True + if o is None: + return False + + if self._name != o._name: + return False + for param in self._params: + if not o._params.__contains__(param): + return False + return True \ No newline at end of file From da9d7dceecfcfe58f6a5e9670697643ab808b662 Mon Sep 17 00:00:00 2001 From: Peter F Date: Wed, 28 Feb 2024 19:41:15 -0500 Subject: [PATCH 088/144] WORKING - two weird bugs with method list - a definition of method __str__ - a neat extra slice in the parser - removal of has_method --- src/umleditor/mvc_model/entity.py | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/src/umleditor/mvc_model/entity.py b/src/umleditor/mvc_model/entity.py index 17320e0b..4ddf4457 100644 --- a/src/umleditor/mvc_model/entity.py +++ b/src/umleditor/mvc_model/entity.py @@ -93,7 +93,7 @@ def rename_field(self, old_field: str, new_field: str): else: self._fields[self._fields.index(old_field)] = new_field - def has_method(self, method_name:str): + def get_method(self, method_name:str): """ Checks if a method exists inside an entity. @@ -108,28 +108,8 @@ def has_method(self, method_name:str): """ for m in self._methods: if m.get_method_name() == method_name: - return True - return False - - def get_method(self, method_name:str): - """ - Gets a method object from inside an entity if it exists. - - Args: - method_name (str): The method's name to be gotten from the entity. - - Raises: - CustomExceptions.MethodNotFoundError: If the method does not - exist in the Entity. - - Returns: - method (UML_Method): The method named method_name. - """ - temp = UML_Method(method_name) - method = self._methods[self._methods.index(temp)] if self.has_method(method_name) else None - if method == None: - raise CustomExceptions.MethodNotFoundError(method_name) - return method + return m + raise CustomExceptions.MethodNotFoundError(method_name) def add_method(self, method_name: str): """ From e56e15f26a6637edcfbb47e823d57730f1727954 Mon Sep 17 00:00:00 2001 From: Peter F Date: Wed, 28 Feb 2024 20:11:47 -0500 Subject: [PATCH 089/144] Bugfixes - removed some print lines that were left over from debugging - made sure that -c works with uneven list sizes --- src/umleditor/mvc_controller/uml_parser.py | 1 - src/umleditor/mvc_model/diagram.py | 4 ---- src/umleditor/mvc_model/entity.py | 1 - 3 files changed, 6 deletions(-) diff --git a/src/umleditor/mvc_controller/uml_parser.py b/src/umleditor/mvc_controller/uml_parser.py index aa5c101c..ff5024bb 100644 --- a/src/umleditor/mvc_controller/uml_parser.py +++ b/src/umleditor/mvc_controller/uml_parser.py @@ -43,7 +43,6 @@ def parse (c, input:str) -> list: args = __split_list(bits) for arg in args: check_args(arg) - print("before return") return [getattr(obj, command_str)] + args #if the args aren't a list, check them as normal diff --git a/src/umleditor/mvc_model/diagram.py b/src/umleditor/mvc_model/diagram.py index 7f8e4c26..f6f83383 100644 --- a/src/umleditor/mvc_model/diagram.py +++ b/src/umleditor/mvc_model/diagram.py @@ -292,7 +292,3 @@ def edit_relation(self, old_src: str, old_dst: str, old_type: str, else: self.add_relation(new_src, new_dst, new_type) self.delete_relation(old_src, old_dst) - - - - diff --git a/src/umleditor/mvc_model/entity.py b/src/umleditor/mvc_model/entity.py index dcff3cc0..63282572 100644 --- a/src/umleditor/mvc_model/entity.py +++ b/src/umleditor/mvc_model/entity.py @@ -276,7 +276,6 @@ def add_parameters(self, params: list[str]): None. """ for new_param in params: - print("outside if") if new_param in self._params: raise CustomExceptions.ParameterExistsError(new_param) self._params.append(new_param) From fffcb12808bf8e68b47f170a2d5665e3b5c7cda1 Mon Sep 17 00:00:00 2001 From: timbmoser <150080066+timbmoser@users.noreply.github.com> Date: Wed, 28 Feb 2024 20:14:18 -0500 Subject: [PATCH 090/144] Update help param change to reflect the | --- src/umleditor/mvc_model/help_command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/umleditor/mvc_model/help_command.py b/src/umleditor/mvc_model/help_command.py index f609a573..c416449d 100644 --- a/src/umleditor/mvc_model/help_command.py +++ b/src/umleditor/mvc_model/help_command.py @@ -39,7 +39,7 @@ def help_menu(): "Parameter Commands:\n\t" "prm -a 'class' 'mthd' 'prm' - adds one or more params to a method (seperate each with a space)\n\t" "prm -d 'class' 'mthd' 'prm' - deletes the param from the method if it exists\n\t" - "prm -c 'class' 'mthd' 'prm' - replaces the params in the method with the new param(s) supplied\n" + "prm -c 'class' 'mthd' 'old' | 'new' - replaces the 'old' param(s) in the method with the 'new' param(s) supplied\n" #Relation Commands "Relation Commands:\n\t" "rel -a 'src' 'dest' 'type' - adds a relationship between class 'src' and class 'dest' of type 'type'\n\t" From 5ef0bfdaadf1ef83cf9e4232d2c8e9d507492269 Mon Sep 17 00:00:00 2001 From: Peter F Date: Wed, 28 Feb 2024 20:28:36 -0500 Subject: [PATCH 091/144] Test Updates - Added special cases to help menu test (to reflect differences in the program) - Removed extra flags from lexer - updated test_help to test_lexer --- src/test/test_help.py | 10 ---------- src/test/test_lexer.py | 15 +++++++++++++++ src/umleditor/mvc_controller/uml_lexer.py | 4 ++-- 3 files changed, 17 insertions(+), 12 deletions(-) delete mode 100644 src/test/test_help.py create mode 100644 src/test/test_lexer.py diff --git a/src/test/test_help.py b/src/test/test_help.py deleted file mode 100644 index a071a171..00000000 --- a/src/test/test_help.py +++ /dev/null @@ -1,10 +0,0 @@ -from umleditor.mvc_controller.uml_lexer import _command_flag_map -from umleditor.mvc_model.help_command import help_menu -import re - -def test_help(): - menu = help_menu() - for key in _command_flag_map: - for flag in _command_flag_map[key]: - val = key + " -" + flag - assert re.search(val, menu) != None, (val + " not in help menu") \ No newline at end of file diff --git a/src/test/test_lexer.py b/src/test/test_lexer.py new file mode 100644 index 00000000..a3168b1f --- /dev/null +++ b/src/test/test_lexer.py @@ -0,0 +1,15 @@ +from umleditor.mvc_controller.uml_lexer import _command_flag_map +from umleditor.mvc_model.help_command import help_menu +import re + +def test_help(): + menu = help_menu() + for key in _command_flag_map: + if key != "help": + for flag in _command_flag_map[key]: + val = "" + if flag != "": + val = key + " -" + flag + else: + val = key + flag + assert re.search(val, menu) != None, (val + " not in help menu") \ No newline at end of file diff --git a/src/umleditor/mvc_controller/uml_lexer.py b/src/umleditor/mvc_controller/uml_lexer.py index 2f50d9a7..80c18ca6 100644 --- a/src/umleditor/mvc_controller/uml_lexer.py +++ b/src/umleditor/mvc_controller/uml_lexer.py @@ -7,7 +7,7 @@ "list" : ["a","c","r","d"], "fld" : ["a","d","r"], "mthd" : ["a","d","r"], - "rel" : ["a","t","d","e"], + "rel" : ["a","t","d"], "save" : [""], "load" : [""], "exit" : [""], @@ -22,7 +22,7 @@ "list" : ["list_everything","list_entities","list_relations","list_entity_details"], "fld" : ["add_field","delete_field","rename_field"], "mthd" : ["add_method","delete_method","rename_method"], - "rel" : ["add_relation","change_relation_type","delete_relation","edit_relation"], + "rel" : ["add_relation","change_relation_type","delete_relation"], "save" : ["save"], "load" : ["load"], "exit" : ["quit"], From 0d2528efab5a13ec92c6f10639d879995499e7cd Mon Sep 17 00:00:00 2001 From: Peter F Date: Wed, 28 Feb 2024 21:05:54 -0500 Subject: [PATCH 092/144] Oops - Re-added edit relation to the lexer - added edit relation to the help menu. It is a long command, but it is also a powerful one. It should be nice to have for 'experienced' CLI users. --- src/umleditor/mvc_controller/uml_lexer.py | 12 ++---------- src/umleditor/mvc_model/help_command.py | 3 ++- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/umleditor/mvc_controller/uml_lexer.py b/src/umleditor/mvc_controller/uml_lexer.py index 3c3e522e..b0b5dbe5 100644 --- a/src/umleditor/mvc_controller/uml_lexer.py +++ b/src/umleditor/mvc_controller/uml_lexer.py @@ -7,13 +7,9 @@ "list" : ["a","c","r","d"], "fld" : ["a","d","r"], "mthd" : ["a","d","r"], -<<<<<<< HEAD - "rel" : ["a","t","d"], -======= "prm" : ["a","d","c"], - "rel" : ["a","t","d"], + "rel" : ["a","t","d", "e"], ->>>>>>> develop "save" : [""], "load" : [""], "exit" : [""], @@ -28,12 +24,8 @@ "list" : ["list_everything","list_entities","list_relations","list_entity_details"], "fld" : ["add_field","delete_field","rename_field"], "mthd" : ["add_method","delete_method","rename_method"], -<<<<<<< HEAD - "rel" : ["add_relation","change_relation_type","delete_relation"], -======= "prm" : ["add_parameters", "remove_parameters", "change_parameters"], - "rel" : ["add_relation", "change_relation_type", "delete_relation"], ->>>>>>> develop + "rel" : ["add_relation", "change_relation_type", "delete_relation", "edit_relation"], "save" : ["save"], "load" : ["load"], "exit" : ["quit"], diff --git a/src/umleditor/mvc_model/help_command.py b/src/umleditor/mvc_model/help_command.py index c416449d..faf673c6 100644 --- a/src/umleditor/mvc_model/help_command.py +++ b/src/umleditor/mvc_model/help_command.py @@ -44,7 +44,8 @@ def help_menu(): "Relation Commands:\n\t" "rel -a 'src' 'dest' 'type' - adds a relationship between class 'src' and class 'dest' of type 'type'\n\t" "rel -t 'src' 'dest' 'type' - changes the type of the relationship between class 'src' and class 'dest' to 'new type'\n\t" - "rel -d 'src' 'dest' - deletes a relationship between class 'src' and class 'dest'\n" + "rel -d 'src' 'dest' - deletes a relationship between class 'src' and class 'dest'\n\t" + "rel -e 'old_src' 'old_dst' 'old_type' 'new_src' 'new_dst' 'new_type' - edits the old relation's fields to be their corresponding new values\n" #List Commands "List Flags: \n\t" "list -a - list all classes and their contents in the UML Diagram\n\t" From 37365ef4c065913a30971e28f4d1f7cf85ad0081 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 29 Feb 2024 19:10:12 -0500 Subject: [PATCH 093/144] Reworked some of the tests in test_diagram to have them run with Pytest. Certain functions are tested more fully in other files and were removed from here. --- src/test/test_diagram.py | 139 +++++++++++++++------------------------ 1 file changed, 54 insertions(+), 85 deletions(-) diff --git a/src/test/test_diagram.py b/src/test/test_diagram.py index 18d74731..2a1548eb 100644 --- a/src/test/test_diagram.py +++ b/src/test/test_diagram.py @@ -1,90 +1,59 @@ -import os - -from umleditor.mvc_model.test import Test -from umleditor.mvc_model.diagram import Diagram -from umleditor.mvc_controller.serializer import CustomJSONEncoder, serialize, deserialize -from umleditor.mvc_model.entity import Entity - -def main(): - """ - Tests the add/rename/delete methods within the Diagram class - """ +from umleditor.mvc_model import Diagram +""" +These test that the basic functions for Diagram interact +with the other classes. The individual classes/functions +are tested more thoroughly in other test files. +""" +def test_create_diagram(): dia = Diagram() - entity = Entity("entityName") - - # list_entities Testing - listTest = Test("list_entities", dia.list_entities) - print(listTest.exec("No entities", "")) - - # add_entity Testing - addTest = Test("add_entity", dia.add_entity) - print(addTest.exec("Valid Name", None, "Entity1")) - print(addTest.exec("Existing Name", "Entity with name 'Entity1' already exists.", "Entity1")) - print(addTest.exec("Existing Name", "Entity with name 'Entity1' already exists.", "Entity1")) - - - # rename_entity Testing - renameTest = Test("rename_entity", dia.rename_entity) - print(renameTest.exec("Valid Rename", None, "Entity1", "NewEntity")) - print(renameTest.exec("Invalid old name", "Entity with name 'Entity!' does not exists.", "Entity!", "NewEntity")) - - # list_entities Testing - listTest = Test("list_entities", dia.list_entities) - print(listTest.exec("Entity exists", "NewEntity")) + assert dia - # SETTING UP STATE FOR RELATION TESTING, STATE: Entity1, Entity2 - dia.rename_entity("NewEntity", "Entity1") - dia.add_entity("Entity2") +def test_dia_add_entity(): + dia = Diagram() + dia.add_entity("ent") + assert dia._entities - # Test add_relation - add_relation_test = Test("add_relation", dia.add_relation) - print(add_relation_test.exec("Relation added", None, "Entity1", "Entity2")) - print(add_relation_test.exec("Relation already exists.", "Relation between 'Entity1 -> Entity2' already exists.", "Entity1", "Entity2")) - - # delete_relation Testing - delete_relation_test = Test("delete_relation", dia.delete_relation) - print(delete_relation_test.exec("Successful deletion", None, "Entity1", "Entity2")) - print(delete_relation_test.exec("No relation.", "Relation between 'Entity1 -> Entity2' does not exist.", "Entity1", "Entity2")) +def test_dia_get_entity(): + dia = Diagram() + assert not dia.has_entity("ent") + dia.add_entity("ent") + ent1 = dia.get_entity("ent") + assert ent1 + assert ent1.get_name() == "ent" + assert ent1.get_name() != "ent1" + assert dia.has_entity("ent") + +def test_dia_delete_entity(): + dia = Diagram() + assert not dia.has_entity("ent1") + dia.add_entity("ent1") + assert dia.has_entity("ent1") + dia.delete_entity("ent1") + assert not dia.has_entity("ent1") - # test delete_entity - dia.add_relation("Entity1", "Entity2") - delete_entityTest = Test("Delete Entity", dia.delete_entity) - print(delete_entityTest.exec("Entity does not exist", "Entity with name 'Entity3' does not exists.", "Entity3")) - print(delete_entityTest.exec("Successful Entity/Relation deletion", None, "Entity1")) - listRelationsTest = Test("List relations", dia.list_relations) - print(listRelationsTest.exec("Relations deleted with entity", "")) - - #add_attribute Testing - addAttrTest = Test("addAttribute", entity.add_attribute) - print(addAttrTest.exec("Valid attribute name", None, "Attribute1")) - print(addAttrTest.exec("Existing attribute", "Attribute with name 'Attribute1' already exists." , "Attribute1" )) - - #rename_attribute Testing - renameAttrTest = Test("renameAttribute", entity.rename_attribute) - print(renameAttrTest.exec("Valid rename attribute", None, "Attribute1", "newAttribute")) - print(renameAttrTest.exec("Invalid old attribute name", "Attribute with name 'Attribute-Name' does not exist.", "Attribute-Name", "newAttribute")) +def test_dia_rename_entity(): + dia = Diagram() + dia.add_entity("ent1") + assert dia.has_entity("ent1") + assert not dia.has_entity("ent2") + dia.rename_entity("ent1", "ent2") + assert not dia.has_entity("ent1") + assert dia.has_entity("ent2") + +def test_dia_add_relation(): + dia = Diagram() + dia.add_entity("ent1") + dia.add_entity("ent2") + assert len(dia._relations) == 0 + dia.add_relation("ent1", "ent2", "aggregation") + assert len(dia._relations) == 1 - #delete_attribute Testing - deleteAtrrTest = Test("deleteAttribute", entity.delete_attribute) - print(deleteAtrrTest.exec("Delete existing attribute", None, "newAttribute")) - print(deleteAtrrTest.exec("Attribute not found", "Attribute with name 'nonExistentAttribute' does not exist.", "nonExistentAttribute")) - - # Save/Load Testing - toSave = Diagram() - toSave.add_entity(name='First') - toSave.add_entity(name='Second') - toSave.add_entity(name='Third') - toSave.add_relation('First', 'Second') - toSave.add_relation('Second', 'First') - toSave.add_relation('First', 'Third') - toSave.add_relation('Second', 'Third') - dirname = os.path.dirname(__file__) - serialize(diagram=toSave, path=os.path.join(dirname, 'save.test')) - toLoad = Diagram() - deserialize(diagram=toLoad, path=os.path.join(dirname, 'save.test')) - res = 'Save/Load - {}' - passed = toSave.list_entities() == toLoad.list_entities() and toSave.list_relations() == toLoad.list_relations() - print(res.format('Passed' if passed else 'Failed')) - -if __name__ == '__main__': - main() +def test_dia_delete_relation(): + dia = Diagram() + dia.add_entity("ent1") + dia.add_entity("ent2") + dia.add_relation("ent1", "ent2", "aggregation") + assert len(dia._relations) == 1 + dia.delete_relation("ent1", "ent2") + assert len(dia._relations) == 0 + \ No newline at end of file From 029bef05c0aa1c3275b6f093f57dd30c13375d0f Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 29 Feb 2024 19:23:47 -0500 Subject: [PATCH 094/144] Added entity equals test, and added an extra assert to test_set_name to confirm the state before set_name was called. --- src/test/test_entity.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/test/test_entity.py b/src/test/test_entity.py index 36f877e6..1576f695 100644 --- a/src/test/test_entity.py +++ b/src/test/test_entity.py @@ -9,9 +9,18 @@ def test_get_name(): assert ent1.get_name() == "entity1" assert ent1.get_name() != "entity2" +def test_entity_equals(): + ent1 = Entity("entity1") + ent2 = Entity("entity2") + assert ent1 == ent1 + assert ent2 == ent2 + assert ent1 != ent2 + assert ent2 != ent1 + def test_set_name(): ent1 = Entity("entity1") assert ent1.get_name() == "entity1" + assert ent1.get_name() != "entity2" ent1.set_name("entity2") assert ent1.get_name() != "entity1" assert ent1.get_name() == "entity2" From c6c7591efb553268154fa074dd323649758b55c2 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 29 Feb 2024 19:10:12 -0500 Subject: [PATCH 095/144] Reworked some of the tests in test_diagram to have them run with Pytest. Certain functions are tested more fully in other files and were removed from here. --- src/test/test_diagram.py | 139 +++++++++++++++------------------------ 1 file changed, 54 insertions(+), 85 deletions(-) diff --git a/src/test/test_diagram.py b/src/test/test_diagram.py index 18d74731..2a1548eb 100644 --- a/src/test/test_diagram.py +++ b/src/test/test_diagram.py @@ -1,90 +1,59 @@ -import os - -from umleditor.mvc_model.test import Test -from umleditor.mvc_model.diagram import Diagram -from umleditor.mvc_controller.serializer import CustomJSONEncoder, serialize, deserialize -from umleditor.mvc_model.entity import Entity - -def main(): - """ - Tests the add/rename/delete methods within the Diagram class - """ +from umleditor.mvc_model import Diagram +""" +These test that the basic functions for Diagram interact +with the other classes. The individual classes/functions +are tested more thoroughly in other test files. +""" +def test_create_diagram(): dia = Diagram() - entity = Entity("entityName") - - # list_entities Testing - listTest = Test("list_entities", dia.list_entities) - print(listTest.exec("No entities", "")) - - # add_entity Testing - addTest = Test("add_entity", dia.add_entity) - print(addTest.exec("Valid Name", None, "Entity1")) - print(addTest.exec("Existing Name", "Entity with name 'Entity1' already exists.", "Entity1")) - print(addTest.exec("Existing Name", "Entity with name 'Entity1' already exists.", "Entity1")) - - - # rename_entity Testing - renameTest = Test("rename_entity", dia.rename_entity) - print(renameTest.exec("Valid Rename", None, "Entity1", "NewEntity")) - print(renameTest.exec("Invalid old name", "Entity with name 'Entity!' does not exists.", "Entity!", "NewEntity")) - - # list_entities Testing - listTest = Test("list_entities", dia.list_entities) - print(listTest.exec("Entity exists", "NewEntity")) + assert dia - # SETTING UP STATE FOR RELATION TESTING, STATE: Entity1, Entity2 - dia.rename_entity("NewEntity", "Entity1") - dia.add_entity("Entity2") +def test_dia_add_entity(): + dia = Diagram() + dia.add_entity("ent") + assert dia._entities - # Test add_relation - add_relation_test = Test("add_relation", dia.add_relation) - print(add_relation_test.exec("Relation added", None, "Entity1", "Entity2")) - print(add_relation_test.exec("Relation already exists.", "Relation between 'Entity1 -> Entity2' already exists.", "Entity1", "Entity2")) - - # delete_relation Testing - delete_relation_test = Test("delete_relation", dia.delete_relation) - print(delete_relation_test.exec("Successful deletion", None, "Entity1", "Entity2")) - print(delete_relation_test.exec("No relation.", "Relation between 'Entity1 -> Entity2' does not exist.", "Entity1", "Entity2")) +def test_dia_get_entity(): + dia = Diagram() + assert not dia.has_entity("ent") + dia.add_entity("ent") + ent1 = dia.get_entity("ent") + assert ent1 + assert ent1.get_name() == "ent" + assert ent1.get_name() != "ent1" + assert dia.has_entity("ent") + +def test_dia_delete_entity(): + dia = Diagram() + assert not dia.has_entity("ent1") + dia.add_entity("ent1") + assert dia.has_entity("ent1") + dia.delete_entity("ent1") + assert not dia.has_entity("ent1") - # test delete_entity - dia.add_relation("Entity1", "Entity2") - delete_entityTest = Test("Delete Entity", dia.delete_entity) - print(delete_entityTest.exec("Entity does not exist", "Entity with name 'Entity3' does not exists.", "Entity3")) - print(delete_entityTest.exec("Successful Entity/Relation deletion", None, "Entity1")) - listRelationsTest = Test("List relations", dia.list_relations) - print(listRelationsTest.exec("Relations deleted with entity", "")) - - #add_attribute Testing - addAttrTest = Test("addAttribute", entity.add_attribute) - print(addAttrTest.exec("Valid attribute name", None, "Attribute1")) - print(addAttrTest.exec("Existing attribute", "Attribute with name 'Attribute1' already exists." , "Attribute1" )) - - #rename_attribute Testing - renameAttrTest = Test("renameAttribute", entity.rename_attribute) - print(renameAttrTest.exec("Valid rename attribute", None, "Attribute1", "newAttribute")) - print(renameAttrTest.exec("Invalid old attribute name", "Attribute with name 'Attribute-Name' does not exist.", "Attribute-Name", "newAttribute")) +def test_dia_rename_entity(): + dia = Diagram() + dia.add_entity("ent1") + assert dia.has_entity("ent1") + assert not dia.has_entity("ent2") + dia.rename_entity("ent1", "ent2") + assert not dia.has_entity("ent1") + assert dia.has_entity("ent2") + +def test_dia_add_relation(): + dia = Diagram() + dia.add_entity("ent1") + dia.add_entity("ent2") + assert len(dia._relations) == 0 + dia.add_relation("ent1", "ent2", "aggregation") + assert len(dia._relations) == 1 - #delete_attribute Testing - deleteAtrrTest = Test("deleteAttribute", entity.delete_attribute) - print(deleteAtrrTest.exec("Delete existing attribute", None, "newAttribute")) - print(deleteAtrrTest.exec("Attribute not found", "Attribute with name 'nonExistentAttribute' does not exist.", "nonExistentAttribute")) - - # Save/Load Testing - toSave = Diagram() - toSave.add_entity(name='First') - toSave.add_entity(name='Second') - toSave.add_entity(name='Third') - toSave.add_relation('First', 'Second') - toSave.add_relation('Second', 'First') - toSave.add_relation('First', 'Third') - toSave.add_relation('Second', 'Third') - dirname = os.path.dirname(__file__) - serialize(diagram=toSave, path=os.path.join(dirname, 'save.test')) - toLoad = Diagram() - deserialize(diagram=toLoad, path=os.path.join(dirname, 'save.test')) - res = 'Save/Load - {}' - passed = toSave.list_entities() == toLoad.list_entities() and toSave.list_relations() == toLoad.list_relations() - print(res.format('Passed' if passed else 'Failed')) - -if __name__ == '__main__': - main() +def test_dia_delete_relation(): + dia = Diagram() + dia.add_entity("ent1") + dia.add_entity("ent2") + dia.add_relation("ent1", "ent2", "aggregation") + assert len(dia._relations) == 1 + dia.delete_relation("ent1", "ent2") + assert len(dia._relations) == 0 + \ No newline at end of file From 6ac80f3a88ce5077409fcdb9afaca25aa50c84d6 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 29 Feb 2024 19:23:47 -0500 Subject: [PATCH 096/144] Added entity equals test, and added an extra assert to test_set_name to confirm the state before set_name was called. --- src/test/test_entity.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/test/test_entity.py b/src/test/test_entity.py index 36f877e6..1576f695 100644 --- a/src/test/test_entity.py +++ b/src/test/test_entity.py @@ -9,9 +9,18 @@ def test_get_name(): assert ent1.get_name() == "entity1" assert ent1.get_name() != "entity2" +def test_entity_equals(): + ent1 = Entity("entity1") + ent2 = Entity("entity2") + assert ent1 == ent1 + assert ent2 == ent2 + assert ent1 != ent2 + assert ent2 != ent1 + def test_set_name(): ent1 = Entity("entity1") assert ent1.get_name() == "entity1" + assert ent1.get_name() != "entity2" ent1.set_name("entity2") assert ent1.get_name() != "entity1" assert ent1.get_name() == "entity2" From dda2b61a0152253f5918325a26ff941b38dff465 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 29 Feb 2024 19:40:30 -0500 Subject: [PATCH 097/144] Mostly reorganized so I could see if things were missing. --- src/test/test_imports.py | 78 +++++++++++++++++++++++----------------- 1 file changed, 46 insertions(+), 32 deletions(-) diff --git a/src/test/test_imports.py b/src/test/test_imports.py index 6b5aab08..aea1d6a5 100644 --- a/src/test/test_imports.py +++ b/src/test/test_imports.py @@ -1,22 +1,9 @@ -def test_import_controller(): - from umleditor.mvc_controller import Controller - assert Controller - -def test_import_diagram(): - from umleditor.mvc_model import Diagram - assert Diagram - -def test_import_entity(): - from umleditor.mvc_model import Entity - assert Entity - -def test_import_relation(): - from umleditor.mvc_model import Relation - assert Relation - -def test_import_method(): - from umleditor.mvc_model import UML_Method - assert UML_Method +#===============================================================================# + #Import From Controller Tests# +#===============================================================================# +def test_import_cli_controller(): + from umleditor.mvc_controller.cli_controller import CLI_Controller + assert CLI_Controller def test_import_controller_input(): from umleditor.mvc_controller.controller_input import read_line, read_file @@ -28,43 +15,70 @@ def test_import_controller_output(): assert write assert write_file +def test_import_controller(): + from umleditor.mvc_controller import Controller + assert Controller + +def test_import_gui_controller(): + from umleditor.mvc_controller.gui_controller import ControllerGUI + assert ControllerGUI + def test_import_serialzer(): from umleditor.mvc_controller.serializer import CustomJSONEncoder, serialize, deserialize assert CustomJSONEncoder assert serialize assert deserialize - + +def test_import_uml_lexer(): + from umleditor.mvc_controller.uml_lexer import _command_flag_map, _command_function_map, lex_input + assert _command_flag_map + assert _command_function_map + assert lex_input + def test_import_uml_parser(): from umleditor.mvc_controller.uml_parser import parse, check_args assert parse assert check_args +#===============================================================================# + #Import From Model Tests# +#===============================================================================# def test_import_custom_exceptions(): from umleditor.mvc_model.custom_exceptions import CustomExceptions assert CustomExceptions +def test_import_diagram(): + from umleditor.mvc_model import Diagram + assert Diagram + +def test_import_entity(): + from umleditor.mvc_model import Entity + assert Entity + def test_import_help(): from umleditor.mvc_model.help_command import help_menu assert help_menu -def test_import_cli_lexer(): - from umleditor.mvc_controller.uml_lexer import _command_flag_map, _command_function_map, lex_input - assert _command_flag_map - assert _command_function_map - assert lex_input +def test_import_method(): + from umleditor.mvc_model import UML_Method + assert UML_Method -def test_import_cli_controller(): - from umleditor.mvc_controller.cli_controller import CLI_Controller - assert CLI_Controller +def test_import_relation(): + from umleditor.mvc_model import Relation + assert Relation -def test_import_gui_controller(): - from umleditor.mvc_controller.gui_controller import ControllerGUI - assert ControllerGUI +#===============================================================================# + #Import From View Tests# +#===============================================================================# def test_import_class_card(): from umleditor.mvc_view.gui_view.class_card import ClassCard assert ClassCard def test_import_class_input_dialog(): from umleditor.mvc_view.gui_view.class_input_dialog import ClassInputDialog - assert ClassInputDialog \ No newline at end of file + assert ClassInputDialog + +def test_import_view_gui(): + from umleditor.mvc_view.gui_view.view_GUI import ViewGUI + assert ViewGUI \ No newline at end of file From 83196bb7863e4fceeef95162a1ace31d97c43a5e Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 29 Feb 2024 20:50:23 -0500 Subject: [PATCH 098/144] Add equals for method test and param tests --- src/test/test_uml_method.py | 142 +++++++++++++++++++++++++++++++++++- 1 file changed, 141 insertions(+), 1 deletion(-) diff --git a/src/test/test_uml_method.py b/src/test/test_uml_method.py index 26a80db9..ef01a441 100644 --- a/src/test/test_uml_method.py +++ b/src/test/test_uml_method.py @@ -18,4 +18,144 @@ def test_set_method_name(): assert md1.get_method_name() != "method2" md1.set_method_name("method2") assert md1.get_method_name() != "method1" - assert md1.get_method_name() == "method2" \ No newline at end of file + assert md1.get_method_name() == "method2" + +def test_method_equals(): + md1 = UML_Method("md1") + md2 = UML_Method("md2") + assert md1 == md1 + assert md1 != md2 + assert md2 == md2 + assert md2 != md1 + +def test_add_parameter(): + md1 = UML_Method("md1") + assert "prm1" not in md1._params + md1.add_parameters(["prm1"]) + assert "prm1" in md1._params + assert "prm2" not in md1._params + md1.add_parameters(["prm2"]) + assert "prm2" in md1._params + +def test_add_multiple_parameters(): + md1 = UML_Method("md1") + assert "prm1" not in md1._params + assert "prm2" not in md1._params + assert "prm3" not in md1._params + assert "prm4" not in md1._params + md1.add_parameters(["prm1", "prm2", "prm3"]) + assert "prm1" in md1._params + assert "prm2" in md1._params + assert "prm3" in md1._params + assert "prm4" not in md1._params + assert "prm5" not in md1._params + md1.add_parameters(["prm4", "prm5"]) + assert "prm1" in md1._params + assert "prm2" in md1._params + assert "prm3" in md1._params + assert "prm4" in md1._params + assert "prm5" in md1._params + +def test_add_one_remove_one_parameter(): + md1 = UML_Method("md1") + md1.add_parameters(["prm1"]) + assert "prm1" in md1._params + md1.remove_parameters(["prm1"]) + assert "prm1" not in md1._params + +def test_add_multiple_remove_one_parameter_at_a_time(): + md1 = UML_Method("md1") + md1.add_parameters(["prm1", "prm2", "prm3", "prm4"]) + assert "prm1" in md1._params + assert "prm2" in md1._params + assert "prm3" in md1._params + assert "prm4" in md1._params + md1.remove_parameters(["prm1"]) + assert "prm1" not in md1._params + assert "prm2" in md1._params + assert "prm3" in md1._params + assert "prm4" in md1._params + md1.remove_parameters(["prm4"]) + assert "prm1" not in md1._params + assert "prm2" in md1._params + assert "prm3" in md1._params + assert "prm4" not in md1._params + md1.remove_parameters(["prm2"]) + assert "prm1" not in md1._params + assert "prm2" not in md1._params + assert "prm3" in md1._params + assert "prm4" not in md1._params + md1.remove_parameters(["prm3"]) + assert "prm1" not in md1._params + assert "prm2" not in md1._params + assert "prm3" not in md1._params + assert "prm4" not in md1._params + +def test_add_multiple_remove_many_params_at_a_time(): + md1 = UML_Method("md1") + md1.add_parameters(["prm1", "prm2", "prm3", "prm4", "prm5", "prm6", "prm7", "prm8"]) + assert "prm1" in md1._params + assert "prm2" in md1._params + assert "prm3" in md1._params + assert "prm4" in md1._params + assert "prm5" in md1._params + assert "prm6" in md1._params + assert "prm7" in md1._params + assert "prm8" in md1._params + md1.remove_parameters(["prm1", "prm2"]) + assert "prm1" not in md1._params + assert "prm2" not in md1._params + assert "prm3" in md1._params + assert "prm4" in md1._params + assert "prm5" in md1._params + assert "prm6" in md1._params + assert "prm7" in md1._params + assert "prm8" in md1._params + md1.remove_parameters(["prm3", "prm5", "prm7"]) + assert "prm1" not in md1._params + assert "prm2" not in md1._params + assert "prm3" not in md1._params + assert "prm4" in md1._params + assert "prm5" not in md1._params + assert "prm6" in md1._params + assert "prm7" not in md1._params + assert "prm8" in md1._params + +def test_change_one_param(): + md1 = UML_Method("md1") + md1.add_parameters(["prm1"]) + assert "prm1" in md1._params + assert "prm2" not in md1._params + md1.change_parameters(["prm1"], ["prm2"]) + assert "prm1" not in md1._params + assert "prm2" in md1._params + +def test_change_multiple_params(): + md1 = UML_Method("md1") + md1.add_parameters(["prm1", "prm2", "prm3", "prm4"]) + assert "prm1" in md1._params + assert "prm2" in md1._params + assert "prm3" in md1._params + assert "prm4" in md1._params + assert "prm5" not in md1._params + assert "prm6" not in md1._params + assert "prm7" not in md1._params + assert "prm8" not in md1._params + md1.change_parameters(["prm1", "prm2"], ["prm5", "prm6"]) + assert "prm1" not in md1._params + assert "prm2" not in md1._params + assert "prm3" in md1._params + assert "prm4" in md1._params + assert "prm5" in md1._params + assert "prm6" in md1._params + assert "prm7" not in md1._params + assert "prm8" not in md1._params + md1.change_parameters(["prm3", "prm4", "prm5"], ["prm7", "prm8", "prm1"]) + assert "prm1" in md1._params + assert "prm2" not in md1._params + assert "prm3" not in md1._params + assert "prm4" not in md1._params + assert "prm5" not in md1._params + assert "prm6" in md1._params + assert "prm7" in md1._params + assert "prm8" in md1._params \ No newline at end of file From a4003b408fddfffa82745ecb5ff5d2abb2357c68 Mon Sep 17 00:00:00 2001 From: almostTaklu Date: Thu, 29 Feb 2024 22:13:48 -0500 Subject: [PATCH 099/144] Fix self-relation error in Diagram class --- main.py | 4 ++-- src/umleditor/mvc_model/custom_exceptions.py | 13 +++++++++++++ src/umleditor/mvc_model/diagram.py | 3 +++ src/umleditor/mvc_model/help_command.py | 1 + src/umleditor/mvc_model/relation.py | 2 +- 5 files changed, 20 insertions(+), 3 deletions(-) diff --git a/main.py b/main.py index d1676afb..365a955c 100644 --- a/main.py +++ b/main.py @@ -48,5 +48,5 @@ def mainGUI(): if not __debug__: debug_main() else: - main() - #mainGUI() + #main() + mainGUI() diff --git a/src/umleditor/mvc_model/custom_exceptions.py b/src/umleditor/mvc_model/custom_exceptions.py index fb2581b6..eeb5d063 100644 --- a/src/umleditor/mvc_model/custom_exceptions.py +++ b/src/umleditor/mvc_model/custom_exceptions.py @@ -87,6 +87,19 @@ class InvalidRelationTypeError(Error): def __init__(self, invalid_type): super().__init__(f"{invalid_type} is not a valid relation type.") + class SelfRelationError(Exception): + """ + Exception raised when a relation is added between an entity and itself. + + Args: entity_name (str): The name of the entity that is trying to relate + to itself. + + + """ + def __init__(self, entity_name): + super().__init__(f"A self-relation for entity '{entity_name}' is not" + f"allowed.") + #===============================================================================# #Method Exceptions #===============================================================================# diff --git a/src/umleditor/mvc_model/diagram.py b/src/umleditor/mvc_model/diagram.py index f6f83383..71b1e7c6 100644 --- a/src/umleditor/mvc_model/diagram.py +++ b/src/umleditor/mvc_model/diagram.py @@ -206,6 +206,9 @@ def add_relation(self,source:str, destination:str, type:str): """ src = self.get_entity(source) dst = self.get_entity(destination) + + if src == dst: + raise CustomExceptions.SelfRelationError(source) to_add = Relation(type, src, dst) for rel in self._relations: diff --git a/src/umleditor/mvc_model/help_command.py b/src/umleditor/mvc_model/help_command.py index c416449d..5f170c42 100644 --- a/src/umleditor/mvc_model/help_command.py +++ b/src/umleditor/mvc_model/help_command.py @@ -43,6 +43,7 @@ def help_menu(): #Relation Commands "Relation Commands:\n\t" "rel -a 'src' 'dest' 'type' - adds a relationship between class 'src' and class 'dest' of type 'type'\n\t" + "Relationship types: 'aggregation', 'composition', 'inheritance', 'realization'\n\t" "rel -t 'src' 'dest' 'type' - changes the type of the relationship between class 'src' and class 'dest' to 'new type'\n\t" "rel -d 'src' 'dest' - deletes a relationship between class 'src' and class 'dest'\n" #List Commands diff --git a/src/umleditor/mvc_model/relation.py b/src/umleditor/mvc_model/relation.py index e8d1939a..9eb190a7 100644 --- a/src/umleditor/mvc_model/relation.py +++ b/src/umleditor/mvc_model/relation.py @@ -11,7 +11,7 @@ def __init__(self, type=next(iter(RELATIONSHIP_TYPE)), source=Entity(), destinat Args: source (Entity): The entity at the start of the relation. destination (Entity): The entity at the end of the relation. - type (str): The type of the relation. + type (str): The type of the relation - ['aggregation', 'composition', 'inheritance', 'realization'] Raises: CustomExceptions.InvalidRelationTypeError: If the type of the relation is From 512ec3037e2f2116c156dfa1b851e18b7dcb155a Mon Sep 17 00:00:00 2001 From: almostTaklu <123041943+almostTaklu@users.noreply.github.com> Date: Thu, 29 Feb 2024 23:59:28 -0500 Subject: [PATCH 100/144] Update help_command.py --- src/umleditor/mvc_model/help_command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/umleditor/mvc_model/help_command.py b/src/umleditor/mvc_model/help_command.py index 236d89e8..74c73f5b 100644 --- a/src/umleditor/mvc_model/help_command.py +++ b/src/umleditor/mvc_model/help_command.py @@ -43,7 +43,7 @@ def help_menu(): #Relation Commands "Relation Commands:\n\t" "rel -a 'src' 'dest' 'type' - adds a relationship between class 'src' and class 'dest' of type 'type'\n\t" - "Relationship types: 'aggregation', 'composition', 'inheritance', 'realization'\n\t" + "\tRelationship types: 'aggregation', 'composition', 'inheritance', 'realization'\n\t" "rel -t 'src' 'dest' 'type' - changes the type of the relationship between class 'src' and class 'dest' to 'new type'\n\t" "rel -d 'src' 'dest' - deletes a relationship between class 'src' and class 'dest'\n\t" "rel -e 'old_src' 'old_dst' 'old_type' 'new_src' 'new_dst' 'new_type' - edits the old relation's fields to be their corresponding new values\n" From b055079d4661cf102f2b0e33f518f67406e4550a Mon Sep 17 00:00:00 2001 From: Peter Date: Fri, 1 Mar 2024 09:10:02 -0500 Subject: [PATCH 101/144] Main Chooses between GUI, debug, CLI correctly --- main.py | 25 ++++++++++++++++------ src/umleditor/mvc_controller/controller.py | 2 +- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/main.py b/main.py index 365a955c..c573ac89 100644 --- a/main.py +++ b/main.py @@ -6,11 +6,24 @@ from PyQt6 import QtWidgets +def main(): + #decides which main to run + which_main = sys.argv[1] if len(sys.argv) > 1 else None + + if which_main == 'cli': + mainCLI() + elif which_main == '-O': + debug_main() + else: + mainGUI() + def debug_main(): + #CLI main without some error catching app = CLI_Controller() - app.run() + app.run() -def main(): +def mainCLI(): + #main CLI execution try: app = CLI_Controller() app.run() @@ -24,7 +37,9 @@ def main(): # Never expect errors to be caught here print('Oh no! Unexpected Error!') + def mainGUI(): + #main gui execution try: # Create QApplication for running the program app = QtWidgets.QApplication(sys.argv) @@ -45,8 +60,4 @@ def mainGUI(): print('Oh no! Unexpected Error!') if __name__ == '__main__': - if not __debug__: - debug_main() - else: - #main() - mainGUI() + main() diff --git a/src/umleditor/mvc_controller/controller.py b/src/umleditor/mvc_controller/controller.py index 7bc1a49b..e66dcd01 100644 --- a/src/umleditor/mvc_controller/controller.py +++ b/src/umleditor/mvc_controller/controller.py @@ -19,7 +19,7 @@ def run(self, line:str) -> str: #parse the command input = parse(self, line) - #return from input is [function object, arg1,...,argn] + #return from parse call is [function object, arg1,...,argn] command = input[0] args = input[1:] From 82a2ff0c5f3adc1c4a4244ea2182d9a9f4d35276 Mon Sep 17 00:00:00 2001 From: Peter Date: Fri, 1 Mar 2024 09:18:11 -0500 Subject: [PATCH 102/144] Fixed the y/N prompt in the build system --- build.py | 1 - 1 file changed, 1 deletion(-) diff --git a/build.py b/build.py index cd00c947..b0617f1b 100644 --- a/build.py +++ b/build.py @@ -3,7 +3,6 @@ def main(): if in_venv(): os.system("pip install -e .") - os.system("pyinstaller main.py") def in_venv() -> bool: '''Checks if the user is currently in a virtual environment From c20d6cdf5a5a1dab6472573f4ae12721ae59ad56 Mon Sep 17 00:00:00 2001 From: Peter Date: Fri, 1 Mar 2024 09:20:41 -0500 Subject: [PATCH 103/144] Build script no longer prompts y/N on rebuild --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index d9a4027f..a159e361 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,6 @@ include_package_data=True, package_dir={'': 'src'}, install_requires = [ - "pyinstaller", "pyqt6", "pytest", ] From cea925ec68e91087ab62a5070f6b17b4fef12937 Mon Sep 17 00:00:00 2001 From: Peter Date: Fri, 1 Mar 2024 09:24:12 -0500 Subject: [PATCH 104/144] Deleting legacy test infrastructure --- src/test/test_parseing.py | 86 --------------------------------- src/test/test_test.py | 40 --------------- src/umleditor/mvc_model/test.py | 80 ------------------------------ 3 files changed, 206 deletions(-) delete mode 100644 src/test/test_parseing.py delete mode 100644 src/test/test_test.py delete mode 100644 src/umleditor/mvc_model/test.py diff --git a/src/test/test_parseing.py b/src/test/test_parseing.py deleted file mode 100644 index 87e3ce4c..00000000 --- a/src/test/test_parseing.py +++ /dev/null @@ -1,86 +0,0 @@ -from umleditor.mvc_controller.controller_input import read_file, read_line -from umleditor.mvc_controller.controller_output import write, write_file -from umleditor.mvc_controller.serializer import CustomJSONEncoder, serialize, deserialize -from umleditor.mvc_model.custom_exceptions import CustomExceptions as CE -from umleditor.mvc_controller.controller import Controller -from umleditor.mvc_model.diagram import Diagram -from umleditor.mvc_model.test import Test -from umleditor.mvc_controller.uml_parser import Parser -import os -# import Help - -#Parser Includes. These will be moved out when the parser is moved. -from umleditor.mvc_model.entity import Entity -from umleditor.mvc_model.relation import Relation - -def main(): - d = Diagram() - c = Parser(d) - unit_tests(c) - - -def unit_tests(c:Parser): - parseTest = Test("Parser Tests", c.parse) - d = c._diagram - - #Class tests - print(parseTest.exec("class add valid name", [d.add_entity, "test"], "class -a test")) - print(parseTest.exec("class add invalid name", CE.InvalidArgumentError("%"), "class -a %")) - print(parseTest.exec("class delete valid name", [d.delete_entity, "test"], "class -d test")) - print(parseTest.exec("class delete invalid name", CE.InvalidArgumentError("%"), "class -d %")) - print(parseTest.exec("class rename valid name", [d.rename_entity, "test", "test2"], "class -r test test2")) - print(parseTest.exec("class delete invalid old name", CE.InvalidArgumentError("%"), "class -r % test")) - print(parseTest.exec("class delete invalid new name", CE.InvalidArgumentError("%"), "class -r test %")) - print(parseTest.exec("class invalid flag", CE.InvalidFlagError("-z", "class"), "class -z test")) - - #List tests - print(parseTest.exec("list everything", [d.list_everything], "list -a")) - print(parseTest.exec("list entities", [d.list_entities], "list -c")) - print(parseTest.exec("list relations", [d.list_relations], "list -r")) - print(parseTest.exec("list detail valid target", [d.list_entity_details, "test"], "list -d test")) - print(parseTest.exec("list detail invalid target", CE.InvalidArgumentError("%"), "list -d %")) - print(parseTest.exec("list invalid flag", CE.InvalidFlagError("-z", "list"), "list -z test")) - - #Attribute tests - d.add_entity("cl") - e = d.get_entity("cl") - print(parseTest.exec("attribute add valid name", [e.add_attribute, "att1"], "att -a cl att1")) - print(parseTest.exec("attribute add invalid class", CE.InvalidArgumentError("'"), "att -a ' att1")) - print(parseTest.exec("attribute add invalid att name", CE.InvalidArgumentError("%"), "att -a cl %")) - print(parseTest.exec("attribute delete valid target", [e.delete_attribute,"att1"], "att -d cl att1")) - print(parseTest.exec("attribute delete invalid class", CE.InvalidArgumentError("'"), "att -d ' att1")) - print(parseTest.exec("attribute delete invalid att name", CE.InvalidArgumentError("'"), "att -a cl '")) - print(parseTest.exec("attribute rename valid", [e.rename_attribute,"att1","att2"], "att -r cl att1 att2")) - print(parseTest.exec("attribute rename invalid class", CE.InvalidArgumentError("'"), "att -r ' att1 att2")) - print(parseTest.exec("attribute rename invalid old name", CE.InvalidArgumentError("'"), "att -r cl ' att2")) - print(parseTest.exec("attribute rename invalid new name", CE.InvalidArgumentError("'"), "att -r cl att1 '")) - print(parseTest.exec("attribute invalid flag", CE.InvalidFlagError("-z", "att"), "att -z test")) - - #Relation tests - print(parseTest.exec("relation add valid args", [d.add_relation, "c1", "c2"], "rel -a c1 c2")) - print(parseTest.exec("relation add invalid source", CE.InvalidArgumentError("'"), "rel -a ' c2")) - print(parseTest.exec("relation add invalid destination", CE.InvalidArgumentError("'"), "rel -a c1 '")) - print(parseTest.exec("relation delete valid args", [d.delete_relation, "c1", "c2"], "rel -d c1 c2")) - print(parseTest.exec("relation delete invalid source", CE.InvalidArgumentError("'"), "rel -d ' c2")) - print(parseTest.exec("relation add invalid destination", CE.InvalidArgumentError("'"), "rel -d c1 '")) - print(parseTest.exec("relation invalid flag", CE.InvalidFlagError("-z", "rel"), "rel -z test")) - - - - - - - - - - - - - - - - - - - -main() \ No newline at end of file diff --git a/src/test/test_test.py b/src/test/test_test.py deleted file mode 100644 index 7ea37dd6..00000000 --- a/src/test/test_test.py +++ /dev/null @@ -1,40 +0,0 @@ -#Tests the class Test.py -from umleditor.mvc_model.test import Test - -class Dummy: - def __init__(self, val): - self.val = val - - def getVal(self): - return self.val - - def setVal(self, newVal): - self.val = newVal - -def addOne(number): - return number + 1 - -def subtractOne(num1, num2): - return num1 - num2 - -def fancyString(string): - return string + " according to all known laws of aviation" - -def main(): - adding = Test("Test addOne", addOne) - print(adding.exec("add one", 2, 1)) - print(adding.exec("add two", 3, 2)) - - subtracting = Test("Test subtract", subtractOne) - print(subtracting.exec("5 - 3", 2, 5, 3)) - print(subtracting.exec("3 - 7", -4, 3, 7)) - - stringmod = Test("fancyString", fancyString) - print(stringmod.exec("idk", "idk according to all known laws of aviation", "idk")) - - pleaseWork = Dummy(5) - dummy = Test("Test Dummy class", pleaseWork.setVal) - print(dummy.checkUpdate("check update", 10, pleaseWork.getVal, 10)) - -main() - diff --git a/src/umleditor/mvc_model/test.py b/src/umleditor/mvc_model/test.py deleted file mode 100644 index 1dd74bb5..00000000 --- a/src/umleditor/mvc_model/test.py +++ /dev/null @@ -1,80 +0,0 @@ -class Test: - def __init__(self, name:str, func): - self.func = func - self.name = name - - - def exec(self, detail:str, expected, *args): - """ - Executes self.func, passing in all args in the order provided - - Args: - detail - a little extra info about what case is being tested - expected - the expected output of this test. Can take any form with a defined __str__ - *args - a variadic list of all arguments that self.func needs to run - - Return: - A string in the form "self.name detail - passed" if the test was passed - A string in the form "self.name detail - expected {} actual {}" if the test was failed - """ - try: - output = self.func(*args) - except Exception as e: - return self.__createOutput(detail, str(expected), str(e)) - - - return self.__createOutput(detail, str(expected), str(output)) - - - def checkUpdate(self, detail:str, expected, searchLoc, *args): - ''' - Executes self.func, passing in all args in the order provided - - Args: - detail - a little extra info about what case is being tested - expected - the expected output of this test. Can take any form with a defined __str__ - searchLoc - the function to call to search for the expected output - *args - a variadic list of all arguments that self.func needs to run - - Return: - A string in the form "self.name detail - passed" if the test was passed - A string in the form "self.name detail - expected {} actual {}" if the test was failed - - ''' - try: - self.func(*args) - except Exception as e: - return self.__createOutput(detail, str(expected), str(e)) - - return self.__createOutput(detail, str(expected), str(searchLoc())) - - '''Private helper that takes an expected and actual output, then checks and formats them. - param detail - a short message about what case is being tested specifically - param expected - the expected outcome to compare to - param actual - the actual outcome of the test - returns a string in the form "self.name detail - passed" if the test was passed - returns a string in the form "self.name detail - expected {} actual {}" if the test was failed - ''' - def __createOutput(self, detail:str, expected:str, actual:str): - ''' - Private helper to create the output string returned from a call to any method that runs a test. - - Args: - detail - a little extra info about what case is being tested - expected - the expected output of this test, as a string - actual - the actual output of this test, as a string - - Return: - A string in the form "self.name detail - passed" if the test was passed - A string in the form "self.name detail - expected {} actual {}" if the test was failed - - ''' - output = self.name + ": " + detail + " - " - - if(expected == actual): - return output + "passed" - else: - return output + "\n\tExpected: " + expected + "\n\tActual: " + actual - - - \ No newline at end of file From 258010b8f8bc31f9dc4b5ee83107c1766b361ed3 Mon Sep 17 00:00:00 2001 From: Peter Date: Fri, 1 Mar 2024 10:04:50 -0500 Subject: [PATCH 105/144] Partially Redone Readme --- README.md | 27 ++++++++++++++++++++---- fe-2.png => assets/open_with_python.png | Bin 2 files changed, 23 insertions(+), 4 deletions(-) rename fe-2.png => assets/open_with_python.png (100%) diff --git a/README.md b/README.md index a173d4aa..bff1e2b5 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,34 @@ This readme contains the steps to launch the **CWorld UML Editor**. This is a terminal based program that allows users to create a Class Diagram with Relations, Classes, and Attributes. Once the program is running, type 'help' for a list of commands. +# Setup Your Environment +The minimum required version of Python to run this program is 3.8. If your version of python, found in the section below, is less than that, please follow [this](https://www.python.org/downloads/) link to install a newer version. + +## Make Sure Python is Installed + +### MacOS +
    +
  1. In the top right corner of your screen, there will be a search bar, a magnifying glass, or both (depending on your version of Mac). Click that. +
  2. Type 'terminal', then hit enter. +
  3. type 'python3 --version' and hit enter. If you don't have developer tools installed, accept the install and wait for it to complete before retyping this command. +
  4. The terminal will print out "Python x.x.x", where x is a number, if python is installed. +
+ +### Windows +
    +
  1. Hold the windows key and click R. +
  2. type 'cmd' and hit enter +
  3. type 'python --version' and hit enter +
  4. + # Steps for Running the Program ## Prerequisite -You can check if you have python installed by entering ```py -V``` or ```python3 --version``` into terminal - +You can check if you have python installed by entering ```py -V``` or ```python3 --version``` into a terminal of your chou -If you don't have python installed (tested on 3.12.2)[ follow the steps here!](https://www.python.org/downloads/) +The minimum required version of python is 3.8. If you don't have python installed, download it [here](https://www.python.org/downloads/). @@ -30,7 +49,7 @@ Windows OS: - Select the ```2024sp-420-CWorld-develop``` folder - Double click the ```main``` file and open with Python -![alt text](fe-2.png) +![alt text](./assets/fe-2.png) MacOS: diff --git a/fe-2.png b/assets/open_with_python.png similarity index 100% rename from fe-2.png rename to assets/open_with_python.png From 744e851f56fa870088fac3bb9e58699dd8bacc08 Mon Sep 17 00:00:00 2001 From: Peter Date: Fri, 1 Mar 2024 10:50:43 -0500 Subject: [PATCH 106/144] Readme updates --- README.md | 53 +++++++++++++++++++++-------------------------------- 1 file changed, 21 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index bff1e2b5..2a5c7f40 100644 --- a/README.md +++ b/README.md @@ -5,61 +5,50 @@ This readme contains the steps to launch the **CWorld UML Editor**. This is a te # Setup Your Environment The minimum required version of Python to run this program is 3.8. If your version of python, found in the section below, is less than that, please follow [this](https://www.python.org/downloads/) link to install a newer version. -## Make Sure Python is Installed +## Check that Python is Installed ### MacOS
    1. In the top right corner of your screen, there will be a search bar, a magnifying glass, or both (depending on your version of Mac). Click that.
    2. Type 'terminal', then hit enter. -
    3. type 'python3 --version' and hit enter. If you don't have developer tools installed, accept the install and wait for it to complete before retyping this command. +
    4. Type 'python3 --version' and hit enter. If you don't have developer tools installed, accept the install and wait for it to complete before retyping this command.
    5. The terminal will print out "Python x.x.x", where x is a number, if python is installed.
    ### Windows
    1. Hold the windows key and click R. -
    2. type 'cmd' and hit enter -
    3. type 'python --version' and hit enter -
    4. - -# Steps for Running the Program - - - -## Prerequisite -You can check if you have python installed by entering ```py -V``` or ```python3 --version``` into a terminal of your chou - -The minimum required version of python is 3.8. If you don't have python installed, download it [here](https://www.python.org/downloads/). +
    5. Type 'cmd' and hit enter +
    6. Type 'py -V' and hit enter +
    7. "Python x.x.x" will print if python is installed. +
    +### Linux +
      +
    1. Open a terminal on your preferred Linux distro. +
    2. Type 'python --version' +
    3. "Python x.x.x" will print if python is installed. +
    +## Install Python +If you do not have python installed, install the latest version for your operating system [here](https://www.python.org/downloads/). -## Step 1 Download Zip +# Download the Project -[Download Zip here!](https://github.com/mucsci-students/2024sp-420-CWorld/archive/refs/heads/main.zip) +## In a Terminal +To dowload the project directly into a terminal, git tools will be required. Follow the instructions [here](https://github.com/git-guides/install-git) to install git if it is not already installed. +## In a Desktop Environment +Download the zip [here](https://github.com/mucsci-students/2024sp-420-CWorld/archive/refs/heads/main.zip) and extract it. -## Step 2 Extract and Locate -Windows OS: +# Build the Project -- Locate downloaded Zip file -- Right click and select extract -- Select "Show extracted files" -- Select extract -- Select the ```2024sp-420-CWorld-develop``` folder -- Double click the ```main``` file and open with Python +Regardless of operating system, this project will install dependencies when it is built. If the build script is run outside a virtual environment, it may modify files on your computer unpredictably. To setup a virtual environment, follow [this link](https://docs.python.org/3/library/venv.html). -![alt text](./assets/fe-2.png) -MacOS: -- Locate downloaded Zip file -- Double click the downloaded Zip file to extract -- Open the extracted folder ```2024sp-420-CWorld-develop``` from the extraction location (same location as your downloaded Zip file) -- Right click on `main.py and open with Python Launcher -## Step 3 Use the Program -Enter ```help``` within the terminal for a list of commands ## Authors From 6f70bb3e139b359611992db15eebdcc0cd8a37a6 Mon Sep 17 00:00:00 2001 From: Peter Date: Fri, 1 Mar 2024 11:07:16 -0500 Subject: [PATCH 107/144] final readme changes --- README.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2a5c7f40..fb9aa29b 100644 --- a/README.md +++ b/README.md @@ -35,10 +35,10 @@ If you do not have python installed, install the latest version for your operati # Download the Project -## In a Terminal +### In a Terminal To dowload the project directly into a terminal, git tools will be required. Follow the instructions [here](https://github.com/git-guides/install-git) to install git if it is not already installed. -## In a Desktop Environment +### In a Desktop Environment Download the zip [here](https://github.com/mucsci-students/2024sp-420-CWorld/archive/refs/heads/main.zip) and extract it. @@ -46,9 +46,15 @@ Download the zip [here](https://github.com/mucsci-students/2024sp-420-CWorld/arc Regardless of operating system, this project will install dependencies when it is built. If the build script is run outside a virtual environment, it may modify files on your computer unpredictably. To setup a virtual environment, follow [this link](https://docs.python.org/3/library/venv.html). +
      +
    1. Open a terminal and navigate to the folder that the project was cloned/extracted into. Basics of terminal navigation can be found at the following links for [Mac](https://www.macworld.com/article/221277/command-line-navigating-files-folders-mac-terminal.html), [Windows PowerShell](https://wiki.communitydata.science/Windows_terminal_navigation), [Windows Command Prompt](https://www.digitalcitizen.life/command-prompt-how-use-basic-commands/), and [Linux](https://www.linode.com/docs/guides/linux-navigation-commands/). +
    2. Once you are in the source directory of the project (its title should be 2024sp-420-CWorld), type 'python build.py'. If you get a warning about not being in a virtual environment, hit enter to exit the script then follow [these](https://docs.python.org/3/library/venv.html) instructions to setup and enter a virtual environment before running 'python build.py' again. +
    3. Type 'python main.py' to run the program in its default mode, or refer to the flags section below this for other options. +
    - - +### Alternate operation modes +'python main.py cli' - runs the program in CLI mode instead of creating a gui +'python main.py -O' - runs the program in CLI debug mode. This mode is nearly identical to the CLI mode, just with slightly less error handling. Use at your own risk. ## Authors From b9392e339c7cc627383e4381e5851676bc95bd31 Mon Sep 17 00:00:00 2001 From: Peter Date: Fri, 1 Mar 2024 11:09:55 -0500 Subject: [PATCH 108/144] probably final help menu updates --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fb9aa29b..5221f452 100644 --- a/README.md +++ b/README.md @@ -52,10 +52,13 @@ Regardless of operating system, this project will install dependencies when it i
  5. Type 'python main.py' to run the program in its default mode, or refer to the flags section below this for other options.
-### Alternate operation modes +### Operation modes +'python main.py' - default operation mode, opens a GUI. 'python main.py cli' - runs the program in CLI mode instead of creating a gui 'python main.py -O' - runs the program in CLI debug mode. This mode is nearly identical to the CLI mode, just with slightly less error handling. Use at your own risk. +**If you are in the CLI mode, type 'help' for a list of commands.** +**In the gui, use the menu options available at the top of the screen and/or by right clicking to manipulate the diagram to your needs** ## Authors Adam Glick-Lynch, Ganga Acharya, Marshall Feng, Peter Freedman, Tim Moser From 619466f3eb331650e6bdf6f826b71a06840c1466 Mon Sep 17 00:00:00 2001 From: Peter Date: Fri, 1 Mar 2024 11:24:16 -0500 Subject: [PATCH 109/144] stupid hyperlinks in lists --- README.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 5221f452..941c7bf0 100644 --- a/README.md +++ b/README.md @@ -46,16 +46,23 @@ Download the zip [here](https://github.com/mucsci-students/2024sp-420-CWorld/arc Regardless of operating system, this project will install dependencies when it is built. If the build script is run outside a virtual environment, it may modify files on your computer unpredictably. To setup a virtual environment, follow [this link](https://docs.python.org/3/library/venv.html). +**The command to execute a python program varies with operating system. On Mac, it is python3. On Windows both py and python work. On Linux it is python. Through the duration of these build instructions, py will be used. Substitute the command appropriate for your operating system in its place.** +
    -
  1. Open a terminal and navigate to the folder that the project was cloned/extracted into. Basics of terminal navigation can be found at the following links for [Mac](https://www.macworld.com/article/221277/command-line-navigating-files-folders-mac-terminal.html), [Windows PowerShell](https://wiki.communitydata.science/Windows_terminal_navigation), [Windows Command Prompt](https://www.digitalcitizen.life/command-prompt-how-use-basic-commands/), and [Linux](https://www.linode.com/docs/guides/linux-navigation-commands/). -
  2. Once you are in the source directory of the project (its title should be 2024sp-420-CWorld), type 'python build.py'. If you get a warning about not being in a virtual environment, hit enter to exit the script then follow [these](https://docs.python.org/3/library/venv.html) instructions to setup and enter a virtual environment before running 'python build.py' again. -
  3. Type 'python main.py' to run the program in its default mode, or refer to the flags section below this for other options. +
  4. Open a terminal and navigate to the folder that the project was cloned/extracted into. Basics of terminal navigation can be found at the links listed below this list. +
  5. Once you are in the source directory of the project (its name should be 2024sp-420-CWorld), type 'py build.py'. If you get a warning about not being in a virtual environment, hit enter to exit the script then follow the instructions at the top of this section to setup and enter a virtual environment before running 'py build.py' again. +
  6. Type 'py main.py' to run the program in its default mode, or refer to the flags section below this for other options.
+[Mac Terminal Navigation](https://www.macworld.com/article/221277/command-line-navigating-files-folders-mac-terminal.html) +[Windows PowerShell Navigation](https://wiki.communitydata.science/Windows_terminal_navigation) +[Windows Command Prompt Navigation](https://www.digitalcitizen.life/command-prompt-how-use-basic-commands/) +[Linux Terminal Navigation](https://www.linode.com/docs/guides/linux-navigation-commands/) + ### Operation modes -'python main.py' - default operation mode, opens a GUI. -'python main.py cli' - runs the program in CLI mode instead of creating a gui -'python main.py -O' - runs the program in CLI debug mode. This mode is nearly identical to the CLI mode, just with slightly less error handling. Use at your own risk. +'py main.py' - default operation mode, opens a GUI. +'py main.py cli' - runs the program in CLI mode instead of creating a gui +'py main.py -O' - runs the program in CLI debug mode. This mode is nearly identical to the CLI mode, just with slightly less error handling. Use at your own risk. **If you are in the CLI mode, type 'help' for a list of commands.** **In the gui, use the menu options available at the top of the screen and/or by right clicking to manipulate the diagram to your needs** From d2153a041ab8128c51c16caa7308e503517462c9 Mon Sep 17 00:00:00 2001 From: Peter Date: Fri, 1 Mar 2024 11:24:53 -0500 Subject: [PATCH 110/144] minor format improvements --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 941c7bf0..a38a3a15 100644 --- a/README.md +++ b/README.md @@ -54,10 +54,10 @@ Regardless of operating system, this project will install dependencies when it i
  • Type 'py main.py' to run the program in its default mode, or refer to the flags section below this for other options. -[Mac Terminal Navigation](https://www.macworld.com/article/221277/command-line-navigating-files-folders-mac-terminal.html) -[Windows PowerShell Navigation](https://wiki.communitydata.science/Windows_terminal_navigation) -[Windows Command Prompt Navigation](https://www.digitalcitizen.life/command-prompt-how-use-basic-commands/) -[Linux Terminal Navigation](https://www.linode.com/docs/guides/linux-navigation-commands/) +[Mac Terminal Navigation](https://www.macworld.com/article/221277/command-line-navigating-files-folders-mac-terminal.html) +[Windows PowerShell Navigation](https://wiki.communitydata.science/Windows_terminal_navigation) +[Windows Command Prompt Navigation](https://www.digitalcitizen.life/command-prompt-how-use-basic-commands/) +[Linux Terminal Navigation](https://www.linode.com/docs/guides/linux-navigation-commands/) ### Operation modes 'py main.py' - default operation mode, opens a GUI. From 8acb41c4039a026656b3002482a6a9ed7098ee3f Mon Sep 17 00:00:00 2001 From: Peter Date: Fri, 1 Mar 2024 11:26:20 -0500 Subject: [PATCH 111/144] be on separate lines pls --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a38a3a15..1c2f831b 100644 --- a/README.md +++ b/README.md @@ -54,10 +54,10 @@ Regardless of operating system, this project will install dependencies when it i
  • Type 'py main.py' to run the program in its default mode, or refer to the flags section below this for other options. -[Mac Terminal Navigation](https://www.macworld.com/article/221277/command-line-navigating-files-folders-mac-terminal.html) -[Windows PowerShell Navigation](https://wiki.communitydata.science/Windows_terminal_navigation) -[Windows Command Prompt Navigation](https://www.digitalcitizen.life/command-prompt-how-use-basic-commands/) -[Linux Terminal Navigation](https://www.linode.com/docs/guides/linux-navigation-commands/) +[Mac Terminal Navigation](https://www.macworld.com/article/221277/command-line-navigating-files-folders-mac-terminal.html) \ +[Windows PowerShell Navigation](https://wiki.communitydata.science/Windows_terminal_navigation) \ +[Windows Command Prompt Navigation](https://www.digitalcitizen.life/command-prompt-how-use-basic-commands/) \ +[Linux Terminal Navigation](https://www.linode.com/docs/guides/linux-navigation-commands/) \ ### Operation modes 'py main.py' - default operation mode, opens a GUI. From 84fbffbf8cd92a2dcce71a775105a839a7eab2b9 Mon Sep 17 00:00:00 2001 From: Peter Date: Fri, 1 Mar 2024 11:26:46 -0500 Subject: [PATCH 112/144] hi --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1c2f831b..f166d601 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ Regardless of operating system, this project will install dependencies when it i [Mac Terminal Navigation](https://www.macworld.com/article/221277/command-line-navigating-files-folders-mac-terminal.html) \ [Windows PowerShell Navigation](https://wiki.communitydata.science/Windows_terminal_navigation) \ [Windows Command Prompt Navigation](https://www.digitalcitizen.life/command-prompt-how-use-basic-commands/) \ -[Linux Terminal Navigation](https://www.linode.com/docs/guides/linux-navigation-commands/) \ +[Linux Terminal Navigation](https://www.linode.com/docs/guides/linux-navigation-commands/) ### Operation modes 'py main.py' - default operation mode, opens a GUI. From c6131c6415a730ae0bfe094d39e5a2c3369fa1b8 Mon Sep 17 00:00:00 2001 From: Peter Date: Fri, 1 Mar 2024 11:28:05 -0500 Subject: [PATCH 113/144] removed unnecessary asset --- assets/open_with_python.png | Bin 24631 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 assets/open_with_python.png diff --git a/assets/open_with_python.png b/assets/open_with_python.png deleted file mode 100644 index 0a71bb707ed8c751bf4d5d5f87e4839c9d8dc276..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24631 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 yK}Q+m{A&385Nt>oe?UbgVK Date: Fri, 1 Mar 2024 11:32:54 -0500 Subject: [PATCH 114/144] added line about testing --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index f166d601..07980f0b 100644 --- a/README.md +++ b/README.md @@ -67,5 +67,8 @@ Regardless of operating system, this project will install dependencies when it i **If you are in the CLI mode, type 'help' for a list of commands.** **In the gui, use the menu options available at the top of the screen and/or by right clicking to manipulate the diagram to your needs** +### Test the project +'pytest' - from the source directory of the project, automatically finds and executes all test files. + ## Authors Adam Glick-Lynch, Ganga Acharya, Marshall Feng, Peter Freedman, Tim Moser From 3d1bbf040ebd614a04e485bd49fb1c870916384c Mon Sep 17 00:00:00 2001 From: almostTaklu Date: Fri, 1 Mar 2024 13:10:02 -0500 Subject: [PATCH 115/144] Add delete functionality to GUI controller and view --- src/umleditor/mvc_controller/gui_controller.py | 14 ++++++++++++-- src/umleditor/mvc_view/gui_view/class_card.py | 9 +++++++++ src/umleditor/mvc_view/gui_view/view_GUI.py | 12 ++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/umleditor/mvc_controller/gui_controller.py b/src/umleditor/mvc_controller/gui_controller.py index 331a012e..7c131313 100644 --- a/src/umleditor/mvc_controller/gui_controller.py +++ b/src/umleditor/mvc_controller/gui_controller.py @@ -52,7 +52,7 @@ def run(self, task: str, widget: QtWidgets): self.add_class(task, widget) # No action required after deleting elif "-d" in task: - return + self.delete_class(task, widget) else: self.acceptance_state(widget) @@ -67,7 +67,17 @@ def add_class(self, task: str, widget: QtWidgets): widget.reject() entity_name = task.split()[-1] self._window.add_class_card(entity_name) - + + def delete_class(self, task: str, widget: QtWidgets): + """ + Deletes class card. + + Parameters: + widget: The widget instance. + """ + class_name = task.split()[-1] + self._window.delete_class_card(class_name) + def acceptance_state(self, widget): """ Makes text read-only and returns diagram to original state diff --git a/src/umleditor/mvc_view/gui_view/class_card.py b/src/umleditor/mvc_view/gui_view/class_card.py index 367578c7..e37d023c 100644 --- a/src/umleditor/mvc_view/gui_view/class_card.py +++ b/src/umleditor/mvc_view/gui_view/class_card.py @@ -100,15 +100,19 @@ def show_class_menu(self, position): """ # Create menu & Actions menu = QMenu() + delete_action = QAction("Delete Class", self) field_action = QAction("Add Field", self) method_action = QAction("Add Method", self) relation_action = QAction("Add Relation", self) + menu.addAction(delete_action) + menu.addSeparator() menu.addAction(field_action) menu.addAction(method_action) menu.addAction(relation_action) # Add button functionality + delete_action.triggered.connect(self.confirm_delete_class) field_action.triggered.connect(lambda: self.menu_action_clicked(self._list_field, "Enter Field")) method_action.triggered.connect(lambda: self.menu_action_clicked(self._list_method, "e.g. method param1 param2...")) relation_action.triggered.connect(lambda: self.menu_action_clicked(self._list_relation, "e.g. dst type")) @@ -194,6 +198,11 @@ def delete_action_clicked(self, widget: QLineEdit): self._enable_widgets_signal.emit(True, self) self.enable_context_menus(True) + def confirm_delete_class(self): + """ + Confirms the deletion of the class. + """ + self._process_task_signal.emit(f"class -d {self._name}", self) def menu_action_clicked(self, list: QListWidget, placeholder: str): """ diff --git a/src/umleditor/mvc_view/gui_view/view_GUI.py b/src/umleditor/mvc_view/gui_view/view_GUI.py index 80e7ce3c..c511a695 100644 --- a/src/umleditor/mvc_view/gui_view/view_GUI.py +++ b/src/umleditor/mvc_view/gui_view/view_GUI.py @@ -107,6 +107,18 @@ def add_class_card(self, name: str): self._size += 1 self._grid_layout.addWidget(class_card, self._row, self._column) + def delete_class_card(self, name: str): + """ + Removes the ClassCard widget for the specified class from the layout. + """ + for i in range(self._grid_layout.count()): + item = self._grid_layout.itemAt(i) + if item is not None: + class_card = item.widget() + if isinstance(class_card, ClassCard) and class_card._name == name: + self._grid_layout.removeWidget(class_card) + class_card.deleteLater() + self._size -= 1 # Decrement the total count of class cards def enable_widgets(self, enabled: bool, active_widget: QWidget): """ From 9dd37b65e66a45c3e6422acbb8c7fddbd24ad952 Mon Sep 17 00:00:00 2001 From: AdamG-L Date: Fri, 1 Mar 2024 14:28:22 -0500 Subject: [PATCH 116/144] Added method and param functionality --- src/umleditor/mvc_controller/uml_lexer.py | 4 ++-- src/umleditor/mvc_model/entity.py | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/umleditor/mvc_controller/uml_lexer.py b/src/umleditor/mvc_controller/uml_lexer.py index b0b5dbe5..7c95bbb0 100644 --- a/src/umleditor/mvc_controller/uml_lexer.py +++ b/src/umleditor/mvc_controller/uml_lexer.py @@ -6,7 +6,7 @@ "class" : ["a","d","r"], "list" : ["a","c","r","d"], "fld" : ["a","d","r"], - "mthd" : ["a","d","r"], + "mthd" : ["a","d","r","ga"], "prm" : ["a","d","c"], "rel" : ["a","t","d", "e"], @@ -23,7 +23,7 @@ "class" : ["add_entity","delete_entity","rename_entity"], "list" : ["list_everything","list_entities","list_relations","list_entity_details"], "fld" : ["add_field","delete_field","rename_field"], - "mthd" : ["add_method","delete_method","rename_method"], + "mthd" : ["add_method","delete_method","rename_method","add_method_and_params"], "prm" : ["add_parameters", "remove_parameters", "change_parameters"], "rel" : ["add_relation", "change_relation_type", "delete_relation", "edit_relation"], "save" : ["save"], diff --git a/src/umleditor/mvc_model/entity.py b/src/umleditor/mvc_model/entity.py index 63282572..0ffb91b8 100644 --- a/src/umleditor/mvc_model/entity.py +++ b/src/umleditor/mvc_model/entity.py @@ -131,6 +131,18 @@ def add_method(self, method_name: str): else: new_method = UML_Method(method_name) self._methods.append(new_method) + + def add_method_and_params(self, method_name: str, params:list[str]): + """ + Adds a method with specified parameters to the class. + + Parameters: + method_name (str): The name of the method to add. + params (list[str]): A list of parameter names for the method. + + """ + self.add_method(method_name) + self.get_method(method_name).add_parameters(params) def delete_method(self, method_name: str): """ From e2e6f72fd03dc4e0b6c0a5d3d1635a9c0d53f785 Mon Sep 17 00:00:00 2001 From: AdamG-L Date: Fri, 1 Mar 2024 15:03:09 -0500 Subject: [PATCH 117/144] Debugging method/params --- src/umleditor/mvc_model/entity.py | 8 ++++++-- src/umleditor/mvc_view/gui_view/class_card.py | 3 +++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/umleditor/mvc_model/entity.py b/src/umleditor/mvc_model/entity.py index 0ffb91b8..a87d98eb 100644 --- a/src/umleditor/mvc_model/entity.py +++ b/src/umleditor/mvc_model/entity.py @@ -132,7 +132,7 @@ def add_method(self, method_name: str): new_method = UML_Method(method_name) self._methods.append(new_method) - def add_method_and_params(self, method_name: str, params:list[str]): + def add_method_and_params(self, method_name: str, params:list[str] = []) : """ Adds a method with specified parameters to the class. @@ -141,8 +141,12 @@ def add_method_and_params(self, method_name: str, params:list[str]): params (list[str]): A list of parameter names for the method. """ + print("WE ARE HERE") self.add_method(method_name) - self.get_method(method_name).add_parameters(params) + # Don't add params if none passed + if not params: + return + #self.get_method(method_name).add_parameters(params) def delete_method(self, method_name: str): """ diff --git a/src/umleditor/mvc_view/gui_view/class_card.py b/src/umleditor/mvc_view/gui_view/class_card.py index e37d023c..261f53f3 100644 --- a/src/umleditor/mvc_view/gui_view/class_card.py +++ b/src/umleditor/mvc_view/gui_view/class_card.py @@ -329,6 +329,9 @@ def verify_input(self, new_text: str, list: QListWidget): class_name + " " + new_text, self) # Method task signals - methodName param1 param2 else: + if self._old_text == "": + words = new_text.split() + self._process_task_signal.emit("mthd -ga " + class_name + " " + " ".join(words), self) pass From 976ae0c3774ef61f32f258b5d4c80e6530e2b53a Mon Sep 17 00:00:00 2001 From: Marshall Feng Date: Fri, 1 Mar 2024 15:09:50 -0500 Subject: [PATCH 118/144] Fix issues of add_method_and_params taking any # of params --- src/umleditor/mvc_model/entity.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/umleditor/mvc_model/entity.py b/src/umleditor/mvc_model/entity.py index a87d98eb..2b1da665 100644 --- a/src/umleditor/mvc_model/entity.py +++ b/src/umleditor/mvc_model/entity.py @@ -132,7 +132,7 @@ def add_method(self, method_name: str): new_method = UML_Method(method_name) self._methods.append(new_method) - def add_method_and_params(self, method_name: str, params:list[str] = []) : + def add_method_and_params(self, method_name: str, *params) : """ Adds a method with specified parameters to the class. @@ -143,10 +143,7 @@ def add_method_and_params(self, method_name: str, params:list[str] = []) : """ print("WE ARE HERE") self.add_method(method_name) - # Don't add params if none passed - if not params: - return - #self.get_method(method_name).add_parameters(params) + self.get_method(method_name).add_parameters([params]) def delete_method(self, method_name: str): """ From f9a2d11f3ffc0dc94cc6963341263ae6fd9764a0 Mon Sep 17 00:00:00 2001 From: AdamG-L Date: Fri, 1 Mar 2024 15:16:34 -0500 Subject: [PATCH 119/144] Added delete method functionality --- src/umleditor/mvc_model/entity.py | 4 +--- src/umleditor/mvc_view/gui_view/class_card.py | 3 +++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/umleditor/mvc_model/entity.py b/src/umleditor/mvc_model/entity.py index 2b1da665..05f0ef2d 100644 --- a/src/umleditor/mvc_model/entity.py +++ b/src/umleditor/mvc_model/entity.py @@ -138,10 +138,8 @@ def add_method_and_params(self, method_name: str, *params) : Parameters: method_name (str): The name of the method to add. - params (list[str]): A list of parameter names for the method. - + *params: Variable-length argument list representing the parameters for the method. """ - print("WE ARE HERE") self.add_method(method_name) self.get_method(method_name).add_parameters([params]) diff --git a/src/umleditor/mvc_view/gui_view/class_card.py b/src/umleditor/mvc_view/gui_view/class_card.py index 261f53f3..1391fae4 100644 --- a/src/umleditor/mvc_view/gui_view/class_card.py +++ b/src/umleditor/mvc_view/gui_view/class_card.py @@ -191,6 +191,9 @@ def delete_action_clicked(self, widget: QLineEdit): elif list_widget is self._list_relation: relation = self.split_relation(widget.text()) self._process_task_signal.emit("rel -d " + class_name + " " + relation[0], self) + else: + method = widget.text().split() + self._process_task_signal.emit("mthd -d " + class_name + " " + method[0], self) list_widget.removeItemWidget(item) list_widget.takeItem(index) return From 44230b974136aabb0251e4803227005f098eb13a Mon Sep 17 00:00:00 2001 From: AdamG-L Date: Fri, 1 Mar 2024 15:44:35 -0500 Subject: [PATCH 120/144] Working edit/add method --- src/umleditor/mvc_controller/uml_lexer.py | 4 ++-- src/umleditor/mvc_model/entity.py | 11 ++++++++++- src/umleditor/mvc_view/gui_view/class_card.py | 8 +++++++- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/umleditor/mvc_controller/uml_lexer.py b/src/umleditor/mvc_controller/uml_lexer.py index 7c95bbb0..c3e9ccf4 100644 --- a/src/umleditor/mvc_controller/uml_lexer.py +++ b/src/umleditor/mvc_controller/uml_lexer.py @@ -6,7 +6,7 @@ "class" : ["a","d","r"], "list" : ["a","c","r","d"], "fld" : ["a","d","r"], - "mthd" : ["a","d","r","ga"], + "mthd" : ["a","d","r","ga", "e"], "prm" : ["a","d","c"], "rel" : ["a","t","d", "e"], @@ -23,7 +23,7 @@ "class" : ["add_entity","delete_entity","rename_entity"], "list" : ["list_everything","list_entities","list_relations","list_entity_details"], "fld" : ["add_field","delete_field","rename_field"], - "mthd" : ["add_method","delete_method","rename_method","add_method_and_params"], + "mthd" : ["add_method","delete_method","rename_method","add_method_and_params","edit_method"], "prm" : ["add_parameters", "remove_parameters", "change_parameters"], "rel" : ["add_relation", "change_relation_type", "delete_relation", "edit_relation"], "save" : ["save"], diff --git a/src/umleditor/mvc_model/entity.py b/src/umleditor/mvc_model/entity.py index 05f0ef2d..b1f32986 100644 --- a/src/umleditor/mvc_model/entity.py +++ b/src/umleditor/mvc_model/entity.py @@ -106,7 +106,7 @@ def get_method(self, method_name:str): None. Returns: - bool: True if the method exists. False if it does not. + UML_Method: Returns method if it exists """ for m in self._methods: if m.get_method_name() == method_name: @@ -143,6 +143,15 @@ def add_method_and_params(self, method_name: str, *params) : self.add_method(method_name) self.get_method(method_name).add_parameters([params]) + def edit_method(self, old_method: str, new_method: str, *params): + deleted_method = self.get_method(old_method) + self.delete_method(old_method) + try: + self.add_method_and_params(new_method, *params) + except Exception as e: + self._methods.append(deleted_method) + raise e + def delete_method(self, method_name: str): """ Deletes a method from this entity if the method exists. diff --git a/src/umleditor/mvc_view/gui_view/class_card.py b/src/umleditor/mvc_view/gui_view/class_card.py index 1391fae4..17ae78de 100644 --- a/src/umleditor/mvc_view/gui_view/class_card.py +++ b/src/umleditor/mvc_view/gui_view/class_card.py @@ -330,11 +330,17 @@ def verify_input(self, new_text: str, list: QListWidget): else: self._process_task_signal.emit("rel -e " + class_name + " " + self._old_text + " " + class_name + " " + new_text, self) - # Method task signals - methodName param1 param2 + # Method task signals else: + # - methodName param1 param2... if self._old_text == "": words = new_text.split() self._process_task_signal.emit("mthd -ga " + class_name + " " + " ".join(words), self) + # - oldName newName param1 param2... + else: + words = new_text.split() + old_name = self._old_text.split()[0] + self._process_task_signal.emit("mthd -e " + class_name + " " + old_name + " " + " ".join(words), self) pass From 59452fd1230b263611e3c64c477faddfb1739f02 Mon Sep 17 00:00:00 2001 From: Marshall Feng Date: Fri, 1 Mar 2024 17:02:54 -0500 Subject: [PATCH 121/144] Fix save/load due to change of entities(dict to list) --- src/umleditor/mvc_controller/serializer.py | 12 +++++++----- src/umleditor/mvc_model/diagram.py | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/umleditor/mvc_controller/serializer.py b/src/umleditor/mvc_controller/serializer.py index eecde1e3..a7824eeb 100644 --- a/src/umleditor/mvc_controller/serializer.py +++ b/src/umleditor/mvc_controller/serializer.py @@ -30,7 +30,7 @@ def serialize(diagram: Diagram, path: str) -> None: ''' # classes saved_classes = [] - for entity in diagram._entities.values(): + for entity in diagram._entities: saved_class = {} # class name saved_class['name'] = entity._name @@ -102,7 +102,7 @@ def deserialize(diagram: Diagram, path: str) -> None: raise CE.JsonDecodeError(filepath=path) try: # classes - loaded_classes = {} + loaded_classes = [] for saved_class in obj['classes']: loaded_class = Entity() # class name @@ -137,16 +137,18 @@ def deserialize(diagram: Diagram, path: str) -> None: loaded_method._params = loaded_params loaded_methods.append(loaded_method) loaded_class._methods = loaded_methods - loaded_classes[loaded_class._name] = loaded_class + loaded_classes.append(loaded_class) diagram._entities = loaded_classes # relationships loaded_relationships = [] for saved_relationship in obj['relationships']: loaded_relationship = Relation() # relationship source - loaded_relationship._source = loaded_classes[saved_relationship['source']] + # loaded_relationship._source = saved_relationship['source'] + loaded_relationship._source = [entity for entity in loaded_classes if entity._name == saved_relationship['source']][0] # relationship destination - loaded_relationship._destination = loaded_classes[saved_relationship['destination']] + # loaded_relationship._destination = saved_relationship['destination'] + loaded_relationship._destination = [entity for entity in loaded_classes if entity._name == saved_relationship['destination']][0] # relationship type loaded_relationship._type = saved_relationship['type'] loaded_relationships.append(loaded_relationship) diff --git a/src/umleditor/mvc_model/diagram.py b/src/umleditor/mvc_model/diagram.py index 71b1e7c6..ba484fa8 100644 --- a/src/umleditor/mvc_model/diagram.py +++ b/src/umleditor/mvc_model/diagram.py @@ -142,7 +142,7 @@ def list_entity_details(self, entity_name): str: A templated string containing the fields, methods, params and relations of an entity. """ - + ent = self.get_entity(entity_name) fields = entity_name +":\n" + entity_name + "'s Fields:\n" + ent.list_fields() + '\n' methods = entity_name + "'s Methods:\n" + ent.list_methods() From fcb60db5d10e34dba31590df529d21435f386ff3 Mon Sep 17 00:00:00 2001 From: Marshall Feng Date: Fri, 1 Mar 2024 17:10:28 -0500 Subject: [PATCH 122/144] Fix save(not saving relations correctly) and add gui save support --- src/test/test_imports.py | 4 +-- .../mvc_controller/gui_controller.py | 14 ++++++++- src/umleditor/mvc_controller/serializer.py | 4 +-- src/umleditor/mvc_view/gui_view/__init__.py | 2 +- .../mvc_view/gui_view/class_input_dialog.py | 6 ++-- src/umleditor/mvc_view/gui_view/view_GUI.py | 30 ++++++++++++++++--- 6 files changed, 47 insertions(+), 13 deletions(-) diff --git a/src/test/test_imports.py b/src/test/test_imports.py index aea1d6a5..1b51f56a 100644 --- a/src/test/test_imports.py +++ b/src/test/test_imports.py @@ -76,8 +76,8 @@ def test_import_class_card(): assert ClassCard def test_import_class_input_dialog(): - from umleditor.mvc_view.gui_view.class_input_dialog import ClassInputDialog - assert ClassInputDialog + from umleditor.mvc_view.gui_view.class_input_dialog import CustomInputDialog + assert CustomInputDialog def test_import_view_gui(): from umleditor.mvc_view.gui_view.view_GUI import ViewGUI diff --git a/src/umleditor/mvc_controller/gui_controller.py b/src/umleditor/mvc_controller/gui_controller.py index 7c131313..a7407342 100644 --- a/src/umleditor/mvc_controller/gui_controller.py +++ b/src/umleditor/mvc_controller/gui_controller.py @@ -4,7 +4,7 @@ from PyQt6.QtWidgets import QInputDialog, QLineEdit from PyQt6.QtCore import QDir from umleditor.mvc_controller.controller import Controller -from umleditor.mvc_view.gui_view.class_input_dialog import ClassInputDialog +from umleditor.mvc_view.gui_view.class_input_dialog import CustomInputDialog from umleditor.mvc_model.custom_exceptions import CustomExceptions as CE class ControllerGUI (Controller): @@ -48,6 +48,9 @@ def run(self, task: str, widget: QtWidgets): self._window.invalid_input_message(str(e)) return # Successful task + if 'save' in task: + self.save_file(widget) + return if "class -a" in task: self.add_class(task, widget) # No action required after deleting @@ -55,6 +58,15 @@ def run(self, task: str, widget: QtWidgets): self.delete_class(task, widget) else: self.acceptance_state(widget) + + def save_file(self, widget: QtWidgets): + """ + Closes dialog and save file. + + Parameters: + widget: ClassInputDialog. + """ + widget.reject() def add_class(self, task: str, widget: QtWidgets): """ diff --git a/src/umleditor/mvc_controller/serializer.py b/src/umleditor/mvc_controller/serializer.py index a7824eeb..820b1f32 100644 --- a/src/umleditor/mvc_controller/serializer.py +++ b/src/umleditor/mvc_controller/serializer.py @@ -70,9 +70,9 @@ def serialize(diagram: Diagram, path: str) -> None: for relation in diagram._relations: saved_relationship = {} # relationship source - saved_relationship['source'] = relation._source + saved_relationship['source'] = relation._source._name # relationship destination - saved_relationship['destination'] = relation._destination + saved_relationship['destination'] = relation._destination._name # relationship type saved_relationship['type'] = relation._type saved_relationships.append(saved_relationship) diff --git a/src/umleditor/mvc_view/gui_view/__init__.py b/src/umleditor/mvc_view/gui_view/__init__.py index d3abaf92..26620a75 100644 --- a/src/umleditor/mvc_view/gui_view/__init__.py +++ b/src/umleditor/mvc_view/gui_view/__init__.py @@ -1,3 +1,3 @@ from .view_GUI import ViewGUI from .class_card import ClassCard -from .class_input_dialog import ClassInputDialog \ No newline at end of file +from .class_input_dialog import CustomInputDialog \ No newline at end of file diff --git a/src/umleditor/mvc_view/gui_view/class_input_dialog.py b/src/umleditor/mvc_view/gui_view/class_input_dialog.py index 91105fc3..814d2c74 100644 --- a/src/umleditor/mvc_view/gui_view/class_input_dialog.py +++ b/src/umleditor/mvc_view/gui_view/class_input_dialog.py @@ -3,10 +3,10 @@ Custom Dialog Box w/ custom accept signal allowing us to check for valid class input ''' -class ClassInputDialog(QDialog): - def __init__(self, parent=None): +class CustomInputDialog(QDialog): + def __init__(self, name, parent=None): super().__init__(parent) - self.setWindowTitle("Add Class") + self.setWindowTitle(name) self.input_text = QLineEdit() self.ok_button = QPushButton("OK") diff --git a/src/umleditor/mvc_view/gui_view/view_GUI.py b/src/umleditor/mvc_view/gui_view/view_GUI.py index c511a695..d0377633 100644 --- a/src/umleditor/mvc_view/gui_view/view_GUI.py +++ b/src/umleditor/mvc_view/gui_view/view_GUI.py @@ -3,7 +3,7 @@ from PyQt6 import uic from PyQt6.QtWidgets import QMessageBox, QWidget, QMenuBar, QGridLayout from PyQt6.QtCore import pyqtSignal -from umleditor.mvc_view.gui_view.class_input_dialog import ClassInputDialog +from umleditor.mvc_view.gui_view.class_input_dialog import CustomInputDialog from umleditor.mvc_view.gui_view.class_card import ClassCard class ViewGUI(QtWidgets.QMainWindow): @@ -44,6 +44,9 @@ def connect_menu(self): Connects menu actions to corresponding methods. """ self._ui.actionAdd_Class.triggered.connect(self.add_class_click) + self._ui.actionSave.triggered.connect(self.save_click) + # self._ui.actionLoad.triggered.connect(self.load_click) + # self._ui.actionExit.triggered.connect(self.exit_click) def invalid_input_message(self, warning: str): """ @@ -69,7 +72,7 @@ def add_class_click(self): """ Opens a dialog for adding a class and connects confirm button """ - self._dialog = ClassInputDialog() + self._dialog = CustomInputDialog(name="Add Class") self._dialog.ok_button.clicked.connect(self.confirm_class_clicked) self._dialog.exec() @@ -120,6 +123,26 @@ def delete_class_card(self, name: str): class_card.deleteLater() self._size -= 1 # Decrement the total count of class cards +################################################################################################## + + def save_click(self): + """ + Opens a dialog for save and connects confirm button + """ + self._dialog = CustomInputDialog('Save') + self._dialog.ok_button.clicked.connect(self.confirm_save_clicked) + self._dialog.exec() + + def confirm_save_clicked(self): + """ + On Confirm emits signal to process task + """ + task = 'save ' + self._dialog.input_text.text() + # Emit signal to controller to handle task + self._process_task_signal.emit(task, self._dialog) + +################################################################################################## + def enable_widgets(self, enabled: bool, active_widget: QWidget): """ Toggles unselected Widgets. False = Disabled, True = Enabled @@ -133,5 +156,4 @@ def enable_widgets(self, enabled: bool, active_widget: QWidget): if enabled or child_widget is active_widget: child_widget.setEnabled(True) else: - child_widget.setEnabled(False) - + child_widget.setEnabled(False) \ No newline at end of file From a85713f6fd88c166f0cf9bd1140804ce37e13ca9 Mon Sep 17 00:00:00 2001 From: Marshall Feng Date: Fri, 1 Mar 2024 17:56:10 -0500 Subject: [PATCH 123/144] Add gui load feature --- .../mvc_controller/gui_controller.py | 27 +++++++ src/umleditor/mvc_view/gui_view/class_card.py | 81 ++++++++++++++++++- src/umleditor/mvc_view/gui_view/view_GUI.py | 35 +++++++- 3 files changed, 141 insertions(+), 2 deletions(-) diff --git a/src/umleditor/mvc_controller/gui_controller.py b/src/umleditor/mvc_controller/gui_controller.py index a7407342..155b5dcb 100644 --- a/src/umleditor/mvc_controller/gui_controller.py +++ b/src/umleditor/mvc_controller/gui_controller.py @@ -51,6 +51,9 @@ def run(self, task: str, widget: QtWidgets): if 'save' in task: self.save_file(widget) return + if 'load' in task: + self.load_file(widget) + return if "class -a" in task: self.add_class(task, widget) # No action required after deleting @@ -67,6 +70,30 @@ def save_file(self, widget: QtWidgets): widget: ClassInputDialog. """ widget.reject() + + def load_file(self, widget: QtWidgets): + """ + Closes dialog and load file. + + Parameters: + widget: ClassInputDialog. + """ + widget.reject() + self._window.delete_all_class_card() + for entity in self._diagram._entities: + class_card = self._window.add_class_card(entity._name) + for field in entity._fields: + class_card.add_field(field) + for method in entity._methods: + s = method.get_method_name() + for param in method._params: + s += ' ' + param + class_card.add_method(s) + for relation in self._diagram._relations: + if relation._source == entity: + s = relation._destination._name + ' ' + relation._type + class_card.add_relation(s) + def add_class(self, task: str, widget: QtWidgets): """ diff --git a/src/umleditor/mvc_view/gui_view/class_card.py b/src/umleditor/mvc_view/gui_view/class_card.py index 17ae78de..2de4bedb 100644 --- a/src/umleditor/mvc_view/gui_view/class_card.py +++ b/src/umleditor/mvc_view/gui_view/class_card.py @@ -392,4 +392,83 @@ def get_enable_signal(self): Returns: pyqtSignal: The signal for widget enabling. """ - return self._enable_widgets_signal \ No newline at end of file + return self._enable_widgets_signal + +####################################################################### + + #load + + def add_field(self, field): + """ + Adds a field. + """ + list = self._list_field + + # Create field and add to list + item = QListWidgetItem() + list.addItem(item) #!!! + text = QLineEdit() + text.setText(field) + text.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + + # Pass the QLineEdit instance + text.customContextMenuRequested.connect(lambda pos: self.show_row_menu(pos, text)) + + # lambda ensures text is only evaluated on enter + text.returnPressed.connect(lambda: self.verify_input(text.text(), list)) + + # Formatting / Style + list.setItemWidget(item, text) + text.setAlignment(Qt.AlignmentFlag.AlignCenter) + + text.setReadOnly(True) + + def add_method(self, method): + """ + Adds a method. + """ + list = self._list_method + + # Create field and add to list + item = QListWidgetItem() + list.addItem(item) #!!! + text = QLineEdit() + text.setText(method) + text.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + + # Pass the QLineEdit instance + text.customContextMenuRequested.connect(lambda pos: self.show_row_menu(pos, text)) + + # lambda ensures text is only evaluated on enter + text.returnPressed.connect(lambda: self.verify_input(text.text(), list)) + + # Formatting / Style + list.setItemWidget(item, text) + text.setAlignment(Qt.AlignmentFlag.AlignCenter) + + text.setReadOnly(True) + + def add_relation(self, relation): + """ + Adds a relation. + """ + list = self._list_relation + + # Create field and add to list + item = QListWidgetItem() + list.addItem(item) #!!! + text = QLineEdit() + text.setText(relation) + text.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + + # Pass the QLineEdit instance + text.customContextMenuRequested.connect(lambda pos: self.show_row_menu(pos, text)) + + # lambda ensures text is only evaluated on enter + text.returnPressed.connect(lambda: self.verify_input(text.text(), list)) + + # Formatting / Style + list.setItemWidget(item, text) + text.setAlignment(Qt.AlignmentFlag.AlignCenter) + + text.setReadOnly(True) \ No newline at end of file diff --git a/src/umleditor/mvc_view/gui_view/view_GUI.py b/src/umleditor/mvc_view/gui_view/view_GUI.py index d0377633..6d83429d 100644 --- a/src/umleditor/mvc_view/gui_view/view_GUI.py +++ b/src/umleditor/mvc_view/gui_view/view_GUI.py @@ -45,7 +45,7 @@ def connect_menu(self): """ self._ui.actionAdd_Class.triggered.connect(self.add_class_click) self._ui.actionSave.triggered.connect(self.save_click) - # self._ui.actionLoad.triggered.connect(self.load_click) + self._ui.actionLoad.triggered.connect(self.load_click) # self._ui.actionExit.triggered.connect(self.exit_click) def invalid_input_message(self, warning: str): @@ -94,6 +94,9 @@ def add_class_card(self, name: str): Args: name (str): The name of the class. + + Returns: + ClassCard: the class card added. """ class_card = ClassCard(name) class_card.get_task_signal().connect(self.forward_signal) @@ -109,6 +112,7 @@ def add_class_card(self, name: str): self._column = 0 self._size += 1 self._grid_layout.addWidget(class_card, self._row, self._column) + return class_card def delete_class_card(self, name: str): """ @@ -122,9 +126,21 @@ def delete_class_card(self, name: str): self._grid_layout.removeWidget(class_card) class_card.deleteLater() self._size -= 1 # Decrement the total count of class cards + + def delete_all_class_card(self): + for i in reversed(range(self._grid_layout.count())): + item = self._grid_layout.itemAt(i) + if item is not None: + class_card = item.widget() + if isinstance(class_card, ClassCard): + print(i) + self._grid_layout.removeWidget(class_card) + class_card.deleteLater() + self._size -= 1 # Decrement the total count of class cards ################################################################################################## + # save def save_click(self): """ Opens a dialog for save and connects confirm button @@ -141,6 +157,23 @@ def confirm_save_clicked(self): # Emit signal to controller to handle task self._process_task_signal.emit(task, self._dialog) + # load + def load_click(self): + """ + Opens a dialog for load and connects confirm button + """ + self._dialog = CustomInputDialog('Load') + self._dialog.ok_button.clicked.connect(self.confirm_load_clicked) + self._dialog.exec() + + def confirm_load_clicked(self): + """ + On Confirm emits signal to process task + """ + task = 'load ' + self._dialog.input_text.text() + # Emit signal to controller to handle task + self._process_task_signal.emit(task, self._dialog) + ################################################################################################## def enable_widgets(self, enabled: bool, active_widget: QWidget): From 4c7cab3c60802d4772cdf7c7b3f404957a9dc466 Mon Sep 17 00:00:00 2001 From: Marshall Feng Date: Fri, 1 Mar 2024 17:59:10 -0500 Subject: [PATCH 124/144] delete uneeded test files --- src/test/auto_test.py | 74 ----------- .../attribute_add_input.txt | 11 -- .../attribute_add_output.txt | 57 --------- .../attribute_delete_input.txt | 12 -- .../attribute_delete_output.txt | 46 ------- .../attribute_rename_input.txt | 12 -- .../attribute_rename_output.txt | 47 ------- .../generated text files/auto_test_list.txt | 15 --- .../generated text files/class_add_input.txt | 8 -- .../generated text files/class_add_output.txt | 42 ------- .../class_delete_input.txt | 11 -- .../class_delete_output.txt | 33 ----- .../class_rename_input.txt | 9 -- .../class_rename_output.txt | 47 ------- src/test/generated text files/exit_input.txt | 13 -- src/test/generated text files/exit_output.txt | 1 - src/test/generated text files/help_input.txt | 1 - src/test/generated text files/help_output.txt | 35 ------ .../generated text files/list_class_input.txt | 12 -- .../list_class_output.txt | 23 ---- .../list_classes_input.txt | 4 - .../list_classes_output.txt | 14 --- .../list_relationships_input.txt | 7 -- .../list_relationships_output.txt | 15 --- src/test/generated text files/load_input.txt | 19 --- src/test/generated text files/load_output.txt | 35 ------ .../relationship_add_input.txt | 16 --- .../relationship_add_output.txt | 115 ------------------ .../relationship_delete_input.txt | 16 --- .../relationship_delete_output.txt | 83 ------------- src/test/generated text files/save_input.txt | 16 --- src/test/generated text files/save_output.txt | 18 --- src/test/test_lexer.py | 15 --- 33 files changed, 882 deletions(-) delete mode 100644 src/test/auto_test.py delete mode 100644 src/test/generated text files/attribute_add_input.txt delete mode 100644 src/test/generated text files/attribute_add_output.txt delete mode 100644 src/test/generated text files/attribute_delete_input.txt delete mode 100644 src/test/generated text files/attribute_delete_output.txt delete mode 100644 src/test/generated text files/attribute_rename_input.txt delete mode 100644 src/test/generated text files/attribute_rename_output.txt delete mode 100644 src/test/generated text files/auto_test_list.txt delete mode 100644 src/test/generated text files/class_add_input.txt delete mode 100644 src/test/generated text files/class_add_output.txt delete mode 100644 src/test/generated text files/class_delete_input.txt delete mode 100644 src/test/generated text files/class_delete_output.txt delete mode 100644 src/test/generated text files/class_rename_input.txt delete mode 100644 src/test/generated text files/class_rename_output.txt delete mode 100644 src/test/generated text files/exit_input.txt delete mode 100644 src/test/generated text files/exit_output.txt delete mode 100644 src/test/generated text files/help_input.txt delete mode 100644 src/test/generated text files/help_output.txt delete mode 100644 src/test/generated text files/list_class_input.txt delete mode 100644 src/test/generated text files/list_class_output.txt delete mode 100644 src/test/generated text files/list_classes_input.txt delete mode 100644 src/test/generated text files/list_classes_output.txt delete mode 100644 src/test/generated text files/list_relationships_input.txt delete mode 100644 src/test/generated text files/list_relationships_output.txt delete mode 100644 src/test/generated text files/load_input.txt delete mode 100644 src/test/generated text files/load_output.txt delete mode 100644 src/test/generated text files/relationship_add_input.txt delete mode 100644 src/test/generated text files/relationship_add_output.txt delete mode 100644 src/test/generated text files/relationship_delete_input.txt delete mode 100644 src/test/generated text files/relationship_delete_output.txt delete mode 100644 src/test/generated text files/save_input.txt delete mode 100644 src/test/generated text files/save_output.txt delete mode 100644 src/test/test_lexer.py diff --git a/src/test/auto_test.py b/src/test/auto_test.py deleted file mode 100644 index 1ce6fe40..00000000 --- a/src/test/auto_test.py +++ /dev/null @@ -1,74 +0,0 @@ -import sys -import os -import subprocess - -# Add parent directory to the Python path -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) - -# Get the current directory -current_dir = os.getcwd() -# Navigate to the parent directory -parent_dir = os.path.abspath(os.path.join(current_dir, os.pardir)) -# Change directory to the parent directory -os.chdir(parent_dir) - -def run(input: str) -> str: - try: - result = subprocess.run(input, capture_output=True, text=True, check=True, shell=True) - except subprocess.CalledProcessError: - return 'Program terminated not as expected!' - return result.stdout - -def generate_execution_cmd(test_name: str) -> str: - return 'cat test/{}_input.txt | python -O main.py'.format(test_name) - -def generate_test_output_file(test_name: str) -> None: - input = generate_execution_cmd(test_name) - output = run(input=input) - open('test/{}_output.txt'.format(test_name), 'w').write(output) - -def test(test_name: str) -> bool: - input = generate_execution_cmd(test_name) - output = run(input=input) - expected = open('test/{}_output.txt'.format(test_name), 'r').read() - return output == expected - -def check_test_input_file_exists(test_name: str): - return os.path.exists('test/{}_input.txt'.format(test_name)) - -def check_test_output_file_exists(test_name: str): - return os.path.exists('test/{}_output.txt'.format(test_name)) - -if __name__ == '__main__': - total = 0 - input_not_found = 0 - passed = 0 - failed = 0 - skipped = 0 - for test_name in open('test/auto_test_list.txt', 'r').read().strip().split('\n'): - total += 1 - if not check_test_input_file_exists(test_name): - print('test: {} - input not found'.format(test_name)) - input_not_found += 1 - continue - if check_test_output_file_exists(test_name): - res = test(test_name) - print('test: {} - {}'.format(test_name, 'passed' if res else 'failed')) - if res: - passed += 1 - else: - failed += 1 - else: - if input('output file of test <{}> does not exist. Type "Yes" to generate one: '.format(test_name)) == 'Yes': - generate_test_output_file(test_name) - print('Output file for test: <{}> has been generated'.format(test_name)) - else: - print('test: {} - skipped'.format(test_name)) - skipped += 1 - print('passed/total: ({}/{})'.format(passed, total)) - print('input_not_found/total: ({}/{})'.format(input_not_found, total)) - print('failed/total: ({}/{})'.format(failed, total)) - print('skipped/total: ({}/{})'.format(skipped, total)) - generated = total - passed - failed - input_not_found - skipped - if generated: - print('generated/total: ({}/{})'.format(generated, total)) \ No newline at end of file diff --git a/src/test/generated text files/attribute_add_input.txt b/src/test/generated text files/attribute_add_input.txt deleted file mode 100644 index e2435db3..00000000 --- a/src/test/generated text files/attribute_add_input.txt +++ /dev/null @@ -1,11 +0,0 @@ -class -a Class1 -class -a Class2 -list -a -att -a Class1 attr1 -list -a -att -a Class1 attr2 -list -a -att -a Class2 attr1 -list -a -att -a NotExist attr1 -list -a \ No newline at end of file diff --git a/src/test/generated text files/attribute_add_output.txt b/src/test/generated text files/attribute_add_output.txt deleted file mode 100644 index 53d3a75a..00000000 --- a/src/test/generated text files/attribute_add_output.txt +++ /dev/null @@ -1,57 +0,0 @@ -Command: Command: Command: -Class1's Attributes: - -Class1's Relations: - - -Class2's Attributes: - -Class2's Relations: - - -Command: Command: -Class1's Attributes: -attr1 -Class1's Relations: - - -Class2's Attributes: - -Class2's Relations: - - -Command: Command: -Class1's Attributes: -attr1, attr2 -Class1's Relations: - - -Class2's Attributes: - -Class2's Relations: - - -Command: Command: -Class1's Attributes: -attr1, attr2 -Class1's Relations: - - -Class2's Attributes: -attr1 -Class2's Relations: - - -Command: Entity with name 'NotExist' does not exist. -Command: -Class1's Attributes: -attr1, attr2 -Class1's Relations: - - -Class2's Attributes: -attr1 -Class2's Relations: - - -Command: \ No newline at end of file diff --git a/src/test/generated text files/attribute_delete_input.txt b/src/test/generated text files/attribute_delete_input.txt deleted file mode 100644 index ea830b7d..00000000 --- a/src/test/generated text files/attribute_delete_input.txt +++ /dev/null @@ -1,12 +0,0 @@ -class -a Class1 -class -a Class2 -att -a Class1 attr1 -att -a Class1 attr2 -att -a Class2 attr1 -list -a -att -d Class1 attr1 -list -a -att -d Class1 attr2 -list -a -att -d Class1 attr1 -list -a \ No newline at end of file diff --git a/src/test/generated text files/attribute_delete_output.txt b/src/test/generated text files/attribute_delete_output.txt deleted file mode 100644 index e91c5bb8..00000000 --- a/src/test/generated text files/attribute_delete_output.txt +++ /dev/null @@ -1,46 +0,0 @@ -Command: Command: Command: Command: Command: Command: -Class1's Attributes: -attr2, attr1 -Class1's Relations: - - -Class2's Attributes: -attr1 -Class2's Relations: - - -Command: Command: -Class1's Attributes: -attr2 -Class1's Relations: - - -Class2's Attributes: -attr1 -Class2's Relations: - - -Command: Command: -Class1's Attributes: - -Class1's Relations: - - -Class2's Attributes: -attr1 -Class2's Relations: - - -Command: Attribute with name 'attr1' does not exist. -Command: -Class1's Attributes: - -Class1's Relations: - - -Class2's Attributes: -attr1 -Class2's Relations: - - -Command: \ No newline at end of file diff --git a/src/test/generated text files/attribute_rename_input.txt b/src/test/generated text files/attribute_rename_input.txt deleted file mode 100644 index e76061d8..00000000 --- a/src/test/generated text files/attribute_rename_input.txt +++ /dev/null @@ -1,12 +0,0 @@ -class -a Class1 -class -a Class2 -att -a Class1 attr1 -att -a Class1 attr2 -att -a Class2 attr1 -list -a -att -r Class1 attr1 attr3 -list -a -att -r Class1 attr2 attr3 -list -a -att -r Class1 attr1 attr2 -list -a \ No newline at end of file diff --git a/src/test/generated text files/attribute_rename_output.txt b/src/test/generated text files/attribute_rename_output.txt deleted file mode 100644 index 2f71c3a2..00000000 --- a/src/test/generated text files/attribute_rename_output.txt +++ /dev/null @@ -1,47 +0,0 @@ -Command: Command: Command: Command: Command: Command: -Class1's Attributes: -attr1, attr2 -Class1's Relations: - - -Class2's Attributes: -attr1 -Class2's Relations: - - -Command: Command: -Class1's Attributes: -attr3, attr2 -Class1's Relations: - - -Class2's Attributes: -attr1 -Class2's Relations: - - -Command: Attribute with name 'attr3' already exists. -Command: -Class1's Attributes: -attr3, attr2 -Class1's Relations: - - -Class2's Attributes: -attr1 -Class2's Relations: - - -Command: Attribute with name 'attr1' does not exist. -Command: -Class1's Attributes: -attr3, attr2 -Class1's Relations: - - -Class2's Attributes: -attr1 -Class2's Relations: - - -Command: \ No newline at end of file diff --git a/src/test/generated text files/auto_test_list.txt b/src/test/generated text files/auto_test_list.txt deleted file mode 100644 index 38fafad3..00000000 --- a/src/test/generated text files/auto_test_list.txt +++ /dev/null @@ -1,15 +0,0 @@ -class_add -class_delete -class_rename -relationship_add -relationship_delete -attribute_add -attribute_delete -attribute_rename -save -load -list_classes -list_class -list_relationships -help -exit \ No newline at end of file diff --git a/src/test/generated text files/class_add_input.txt b/src/test/generated text files/class_add_input.txt deleted file mode 100644 index fa6be086..00000000 --- a/src/test/generated text files/class_add_input.txt +++ /dev/null @@ -1,8 +0,0 @@ -class -a GoodName -list -a -class -a GoodName123 -list -a -class -a BadName^& -list -a -class -a Two Words -list -a \ No newline at end of file diff --git a/src/test/generated text files/class_add_output.txt b/src/test/generated text files/class_add_output.txt deleted file mode 100644 index bba3b95e..00000000 --- a/src/test/generated text files/class_add_output.txt +++ /dev/null @@ -1,42 +0,0 @@ -Command: Command: -GoodName's Attributes: - -GoodName's Relations: - - -Command: Command: -GoodName's Attributes: - -GoodName's Relations: - - -GoodName123's Attributes: - -GoodName123's Relations: - - -Command: Argument 'BadName^&' is not alphanumeric. -Command: -GoodName's Attributes: - -GoodName's Relations: - - -GoodName123's Attributes: - -GoodName123's Relations: - - -Command: Expected 2 arguments, but 3 were given. -Command: -GoodName's Attributes: - -GoodName's Relations: - - -GoodName123's Attributes: - -GoodName123's Relations: - - -Command: \ No newline at end of file diff --git a/src/test/generated text files/class_delete_input.txt b/src/test/generated text files/class_delete_input.txt deleted file mode 100644 index 05735f8e..00000000 --- a/src/test/generated text files/class_delete_input.txt +++ /dev/null @@ -1,11 +0,0 @@ -class -a Class1 -class -a Class2 -list -a -class -d Class1 -list -a -class -d NotExist -list -a -class -d Two Words -list -a -class -d Class2 -list -a \ No newline at end of file diff --git a/src/test/generated text files/class_delete_output.txt b/src/test/generated text files/class_delete_output.txt deleted file mode 100644 index 250e7fc9..00000000 --- a/src/test/generated text files/class_delete_output.txt +++ /dev/null @@ -1,33 +0,0 @@ -Command: Command: Command: -Class1's Attributes: - -Class1's Relations: - - -Class2's Attributes: - -Class2's Relations: - - -Command: Command: -Class2's Attributes: - -Class2's Relations: - - -Command: Entity with name 'NotExist' does not exist. -Command: -Class2's Attributes: - -Class2's Relations: - - -Command: Expected 2 arguments, but 3 were given. -Command: -Class2's Attributes: - -Class2's Relations: - - -Command: Command: -Command: \ No newline at end of file diff --git a/src/test/generated text files/class_rename_input.txt b/src/test/generated text files/class_rename_input.txt deleted file mode 100644 index 7742a123..00000000 --- a/src/test/generated text files/class_rename_input.txt +++ /dev/null @@ -1,9 +0,0 @@ -class -a Class1 -class -a Class2 -list -a -class -r Class1 Class3 -list -a -class -r Class1 Class4 -list -a -class -r NotExist Class4 -list -a \ No newline at end of file diff --git a/src/test/generated text files/class_rename_output.txt b/src/test/generated text files/class_rename_output.txt deleted file mode 100644 index 7e7e4f37..00000000 --- a/src/test/generated text files/class_rename_output.txt +++ /dev/null @@ -1,47 +0,0 @@ -Command: Command: Command: -Class1's Attributes: - -Class1's Relations: - - -Class2's Attributes: - -Class2's Relations: - - -Command: Command: -Class2's Attributes: - -Class2's Relations: - - -Class3's Attributes: - -Class3's Relations: - - -Command: Entity with name 'Class1' does not exist. -Command: -Class2's Attributes: - -Class2's Relations: - - -Class3's Attributes: - -Class3's Relations: - - -Command: Entity with name 'NotExist' does not exist. -Command: -Class2's Attributes: - -Class2's Relations: - - -Class3's Attributes: - -Class3's Relations: - - -Command: \ No newline at end of file diff --git a/src/test/generated text files/exit_input.txt b/src/test/generated text files/exit_input.txt deleted file mode 100644 index 1282022e..00000000 --- a/src/test/generated text files/exit_input.txt +++ /dev/null @@ -1,13 +0,0 @@ -class -a Class1 -class -a Class2 -class -a Class3 -rel -a Class1 Class2 -rel -a Class1 Class3 -rel -a Class2 Class1 -rel -a Class1 Class1 -att -a Class1 attr1 -att -a Class1 attr2 -att -a Class2 attr1 -quit - -AutoTest \ No newline at end of file diff --git a/src/test/generated text files/exit_output.txt b/src/test/generated text files/exit_output.txt deleted file mode 100644 index af810130..00000000 --- a/src/test/generated text files/exit_output.txt +++ /dev/null @@ -1 +0,0 @@ -Command: Command: Command: Command: Command: Command: Command: Command: Command: Command: Command: Would you like to save before quit? [Y]/n: Name of file to save: \ No newline at end of file diff --git a/src/test/generated text files/help_input.txt b/src/test/generated text files/help_input.txt deleted file mode 100644 index 4cea3acc..00000000 --- a/src/test/generated text files/help_input.txt +++ /dev/null @@ -1 +0,0 @@ -help \ No newline at end of file diff --git a/src/test/generated text files/help_output.txt b/src/test/generated text files/help_output.txt deleted file mode 100644 index 479af983..00000000 --- a/src/test/generated text files/help_output.txt +++ /dev/null @@ -1,35 +0,0 @@ -Command: -Help menu: For the best view, resize your window so that this message and the bar at the end are on one line. | - -Below are the commands you can call and an explanation of what each does. Anything inside single quotes is decided -by you! Enter the command, replacing anything in the single quotes, and the quotes themselves, with the name you -want to use. - -A valid name is made up of any combination of letters and numbers. - -Class Commands: - class -a 'name' - adds a class with name 'name'. Cannot add classes with duplicate or invalid names - class -d 'name' - deletes a class with name 'name' - class -r 'old' 'new' - renames class 'old' to 'new'. Cannot rename classes to duplicate or invalid names -Attribute Commands: - att -a class 'name' - adds an attribute with name 'name' to class 'class' - att -d class 'name' - deletes an attribute with name 'name' from class 'class' if one exists - att -r class 'old' 'new' - renames an attribute from name 'old' to name 'new' in class 'class' -Relation Commands: - rel -a 'src' 'dest' - adds a relationship between class 'src' and class 'dest' assuming both are valid - rel -d 'src' 'dest' - deletes a relationship between class 'src' and class 'dest' if one exists -List Flags: - list -a - list all classes and their contents in the UML Diagram - list -c - list all classes in the UML Diagram - list -r - list all relationships in the UML Diagram - list -d 'name' - list all contents of class 'name' -Save Flags: - save 'name' - saves the UML Diagram as a JSON file with name 'name' -Load Flags: - load 'name' - loads the file with name 'name.json' if one exists. - -Exit Commands: - exit - terminates the program. - quit - terminates the program. - -Command: \ No newline at end of file diff --git a/src/test/generated text files/list_class_input.txt b/src/test/generated text files/list_class_input.txt deleted file mode 100644 index d0fd7628..00000000 --- a/src/test/generated text files/list_class_input.txt +++ /dev/null @@ -1,12 +0,0 @@ -class -a Class1 -class -a Class2 -rel -a Class1 Class2 -rel -a Class2 Class1 -rel -a Class1 Class1 -att -a Class1 attr1 -att -a Class1 attr2 -att -a Class2 attr1 -list -a -list -d Class1 -list -d Class2 -list -d NotExist \ No newline at end of file diff --git a/src/test/generated text files/list_class_output.txt b/src/test/generated text files/list_class_output.txt deleted file mode 100644 index b24ac80d..00000000 --- a/src/test/generated text files/list_class_output.txt +++ /dev/null @@ -1,23 +0,0 @@ -Command: Command: Command: Command: Command: Command: Command: Command: Command: -Class1's Attributes: -attr2, attr1 -Class1's Relations: -Class1 -> Class2, Class2 -> Class1, Class1 -> Class1 - -Class2's Attributes: -attr1 -Class2's Relations: -Class1 -> Class2, Class2 -> Class1 - -Command: -Class1's Attributes: -attr2, attr1 -Class1's Relations: -Class1 -> Class2, Class2 -> Class1, Class1 -> Class1 -Command: -Class2's Attributes: -attr1 -Class2's Relations: -Class1 -> Class2, Class2 -> Class1 -Command: Entity with name 'NotExist' does not exist. -Command: \ No newline at end of file diff --git a/src/test/generated text files/list_classes_input.txt b/src/test/generated text files/list_classes_input.txt deleted file mode 100644 index 7209e9d8..00000000 --- a/src/test/generated text files/list_classes_input.txt +++ /dev/null @@ -1,4 +0,0 @@ -class -a Class1 -class -a Class2 -list -a -list -c \ No newline at end of file diff --git a/src/test/generated text files/list_classes_output.txt b/src/test/generated text files/list_classes_output.txt deleted file mode 100644 index 5df7100b..00000000 --- a/src/test/generated text files/list_classes_output.txt +++ /dev/null @@ -1,14 +0,0 @@ -Command: Command: Command: -Class1's Attributes: - -Class1's Relations: - - -Class2's Attributes: - -Class2's Relations: - - -Command: -Class1, Class2 -Command: \ No newline at end of file diff --git a/src/test/generated text files/list_relationships_input.txt b/src/test/generated text files/list_relationships_input.txt deleted file mode 100644 index 80a048de..00000000 --- a/src/test/generated text files/list_relationships_input.txt +++ /dev/null @@ -1,7 +0,0 @@ -class -a Class1 -class -a Class2 -rel -a Class1 Class2 -rel -a Class2 Class1 -rel -a Class1 Class1 -list -a -list -r \ No newline at end of file diff --git a/src/test/generated text files/list_relationships_output.txt b/src/test/generated text files/list_relationships_output.txt deleted file mode 100644 index 0dccf10a..00000000 --- a/src/test/generated text files/list_relationships_output.txt +++ /dev/null @@ -1,15 +0,0 @@ -Command: Command: Command: Command: Command: Command: -Class1's Attributes: - -Class1's Relations: -Class1 -> Class2, Class2 -> Class1, Class1 -> Class1 - -Class2's Attributes: - -Class2's Relations: -Class1 -> Class2, Class2 -> Class1 - -Command: Class1 -> Class2 -Class2 -> Class1 -Class1 -> Class1 -Command: \ No newline at end of file diff --git a/src/test/generated text files/load_input.txt b/src/test/generated text files/load_input.txt deleted file mode 100644 index 2b39ee45..00000000 --- a/src/test/generated text files/load_input.txt +++ /dev/null @@ -1,19 +0,0 @@ -class -a Class1 -class -a Class2 -class -a Class3 -rel -a Class1 Class2 -rel -a Class1 Class3 -rel -a Class2 Class1 -rel -a Class1 Class1 -att -a Class1 attr1 -att -a Class1 attr2 -att -a Class2 attr1 -save AutoTest -class -d Class1 -class -d Class2 -class -d Class3 -list -a -load AutoTest -list -a -load NotExistAutoTest -list -a \ No newline at end of file diff --git a/src/test/generated text files/load_output.txt b/src/test/generated text files/load_output.txt deleted file mode 100644 index a258a356..00000000 --- a/src/test/generated text files/load_output.txt +++ /dev/null @@ -1,35 +0,0 @@ -Command: Command: Command: Command: Command: Command: Command: Command: Command: Command: Command: Command: Command: Command: Command: -Command: Command: -Class1's Attributes: -attr2, attr1 -Class1's Relations: -Class1 -> Class2, Class1 -> Class3, Class2 -> Class1, Class1 -> Class1 - -Class2's Attributes: -attr1 -Class2's Relations: -Class1 -> Class2, Class2 -> Class1 - -Class3's Attributes: - -Class3's Relations: -Class1 -> Class3 - -Command: Can not read file: "/home/Mars/2024sp-420-CWorld/save/NotExistAutoTest.json". -Command: -Class1's Attributes: -attr2, attr1 -Class1's Relations: -Class1 -> Class2, Class1 -> Class3, Class2 -> Class1, Class1 -> Class1 - -Class2's Attributes: -attr1 -Class2's Relations: -Class1 -> Class2, Class2 -> Class1 - -Class3's Attributes: - -Class3's Relations: -Class1 -> Class3 - -Command: \ No newline at end of file diff --git a/src/test/generated text files/relationship_add_input.txt b/src/test/generated text files/relationship_add_input.txt deleted file mode 100644 index e2f8d179..00000000 --- a/src/test/generated text files/relationship_add_input.txt +++ /dev/null @@ -1,16 +0,0 @@ -class -a Class1 -class -a Class2 -class -a Class3 -list -a -rel -a Class1 Class2 -list -a -rel -a Class1 Class3 -list -a -rel -a Class2 Class1 -list -a -rel -a Class1 Class2 -list -a -rel -a Class1 Class1 -list -a -rel -a NotExist Class1 -list -a \ No newline at end of file diff --git a/src/test/generated text files/relationship_add_output.txt b/src/test/generated text files/relationship_add_output.txt deleted file mode 100644 index 9788cfed..00000000 --- a/src/test/generated text files/relationship_add_output.txt +++ /dev/null @@ -1,115 +0,0 @@ -Command: Command: Command: Command: -Class1's Attributes: - -Class1's Relations: - - -Class2's Attributes: - -Class2's Relations: - - -Class3's Attributes: - -Class3's Relations: - - -Command: Command: -Class1's Attributes: - -Class1's Relations: -Class1 -> Class2 - -Class2's Attributes: - -Class2's Relations: -Class1 -> Class2 - -Class3's Attributes: - -Class3's Relations: - - -Command: Command: -Class1's Attributes: - -Class1's Relations: -Class1 -> Class2, Class1 -> Class3 - -Class2's Attributes: - -Class2's Relations: -Class1 -> Class2 - -Class3's Attributes: - -Class3's Relations: -Class1 -> Class3 - -Command: Command: -Class1's Attributes: - -Class1's Relations: -Class1 -> Class2, Class1 -> Class3, Class2 -> Class1 - -Class2's Attributes: - -Class2's Relations: -Class1 -> Class2, Class2 -> Class1 - -Class3's Attributes: - -Class3's Relations: -Class1 -> Class3 - -Command: Relation between 'Class1 -> Class2' already exists. -Command: -Class1's Attributes: - -Class1's Relations: -Class1 -> Class2, Class1 -> Class3, Class2 -> Class1 - -Class2's Attributes: - -Class2's Relations: -Class1 -> Class2, Class2 -> Class1 - -Class3's Attributes: - -Class3's Relations: -Class1 -> Class3 - -Command: Command: -Class1's Attributes: - -Class1's Relations: -Class1 -> Class2, Class1 -> Class3, Class2 -> Class1, Class1 -> Class1 - -Class2's Attributes: - -Class2's Relations: -Class1 -> Class2, Class2 -> Class1 - -Class3's Attributes: - -Class3's Relations: -Class1 -> Class3 - -Command: Entity with name 'NotExist' does not exist. -Command: -Class1's Attributes: - -Class1's Relations: -Class1 -> Class2, Class1 -> Class3, Class2 -> Class1, Class1 -> Class1 - -Class2's Attributes: - -Class2's Relations: -Class1 -> Class2, Class2 -> Class1 - -Class3's Attributes: - -Class3's Relations: -Class1 -> Class3 - -Command: \ No newline at end of file diff --git a/src/test/generated text files/relationship_delete_input.txt b/src/test/generated text files/relationship_delete_input.txt deleted file mode 100644 index 60d1ce79..00000000 --- a/src/test/generated text files/relationship_delete_input.txt +++ /dev/null @@ -1,16 +0,0 @@ -class -a Class1 -class -a Class2 -class -a Class3 -rel -a Class1 Class2 -rel -a Class1 Class3 -rel -a Class2 Class1 -rel -a Class1 Class1 -list -a -rel -d Class1 Class2 -list -a -rel -d Class1 Class1 -list -a -rel -d Class1 Class2 -list -a -rel -d NotExist Class1 -list -a \ No newline at end of file diff --git a/src/test/generated text files/relationship_delete_output.txt b/src/test/generated text files/relationship_delete_output.txt deleted file mode 100644 index 53447ab9..00000000 --- a/src/test/generated text files/relationship_delete_output.txt +++ /dev/null @@ -1,83 +0,0 @@ -Command: Command: Command: Command: Command: Command: Command: Command: -Class1's Attributes: - -Class1's Relations: -Class1 -> Class2, Class1 -> Class3, Class2 -> Class1, Class1 -> Class1 - -Class2's Attributes: - -Class2's Relations: -Class1 -> Class2, Class2 -> Class1 - -Class3's Attributes: - -Class3's Relations: -Class1 -> Class3 - -Command: Command: -Class1's Attributes: - -Class1's Relations: -Class1 -> Class3, Class2 -> Class1, Class1 -> Class1 - -Class2's Attributes: - -Class2's Relations: -Class2 -> Class1 - -Class3's Attributes: - -Class3's Relations: -Class1 -> Class3 - -Command: Command: -Class1's Attributes: - -Class1's Relations: -Class1 -> Class3, Class2 -> Class1 - -Class2's Attributes: - -Class2's Relations: -Class2 -> Class1 - -Class3's Attributes: - -Class3's Relations: -Class1 -> Class3 - -Command: Relation between 'Class1 -> Class2' does not exist. -Command: -Class1's Attributes: - -Class1's Relations: -Class1 -> Class3, Class2 -> Class1 - -Class2's Attributes: - -Class2's Relations: -Class2 -> Class1 - -Class3's Attributes: - -Class3's Relations: -Class1 -> Class3 - -Command: Entity with name 'NotExist' does not exist. -Command: -Class1's Attributes: - -Class1's Relations: -Class1 -> Class3, Class2 -> Class1 - -Class2's Attributes: - -Class2's Relations: -Class2 -> Class1 - -Class3's Attributes: - -Class3's Relations: -Class1 -> Class3 - -Command: \ No newline at end of file diff --git a/src/test/generated text files/save_input.txt b/src/test/generated text files/save_input.txt deleted file mode 100644 index 3cc09f03..00000000 --- a/src/test/generated text files/save_input.txt +++ /dev/null @@ -1,16 +0,0 @@ -class -a Class1 -class -a Class2 -class -a Class3 -rel -a Class1 Class2 -rel -a Class1 Class3 -rel -a Class2 Class1 -rel -a Class1 Class1 -att -a Class1 attr1 -att -a Class1 attr2 -att -a Class2 attr1 -list -a -save AutoTest -class -d Class1 -class -d Class2 -class -d Class3 -list -a \ No newline at end of file diff --git a/src/test/generated text files/save_output.txt b/src/test/generated text files/save_output.txt deleted file mode 100644 index fbaa87fe..00000000 --- a/src/test/generated text files/save_output.txt +++ /dev/null @@ -1,18 +0,0 @@ -Command: Command: Command: Command: Command: Command: Command: Command: Command: Command: Command: -Class1's Attributes: -attr2, attr1 -Class1's Relations: -Class1 -> Class2, Class1 -> Class3, Class2 -> Class1, Class1 -> Class1 - -Class2's Attributes: -attr1 -Class2's Relations: -Class1 -> Class2, Class2 -> Class1 - -Class3's Attributes: - -Class3's Relations: -Class1 -> Class3 - -Command: Command: Command: Command: Command: -Command: \ No newline at end of file diff --git a/src/test/test_lexer.py b/src/test/test_lexer.py deleted file mode 100644 index a3168b1f..00000000 --- a/src/test/test_lexer.py +++ /dev/null @@ -1,15 +0,0 @@ -from umleditor.mvc_controller.uml_lexer import _command_flag_map -from umleditor.mvc_model.help_command import help_menu -import re - -def test_help(): - menu = help_menu() - for key in _command_flag_map: - if key != "help": - for flag in _command_flag_map[key]: - val = "" - if flag != "": - val = key + " -" + flag - else: - val = key + flag - assert re.search(val, menu) != None, (val + " not in help menu") \ No newline at end of file From f79ce35103d8505b9d2f748c8edbf70da7f2f172 Mon Sep 17 00:00:00 2001 From: Marshall Feng Date: Fri, 1 Mar 2024 18:11:42 -0500 Subject: [PATCH 125/144] Add gui exit feature --- src/umleditor/mvc_view/gui_view/view_GUI.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/umleditor/mvc_view/gui_view/view_GUI.py b/src/umleditor/mvc_view/gui_view/view_GUI.py index 6d83429d..dddc3d5f 100644 --- a/src/umleditor/mvc_view/gui_view/view_GUI.py +++ b/src/umleditor/mvc_view/gui_view/view_GUI.py @@ -1,4 +1,5 @@ import os +import sys from PyQt6 import QtCore, QtGui, QtWidgets from PyQt6 import uic from PyQt6.QtWidgets import QMessageBox, QWidget, QMenuBar, QGridLayout @@ -46,7 +47,7 @@ def connect_menu(self): self._ui.actionAdd_Class.triggered.connect(self.add_class_click) self._ui.actionSave.triggered.connect(self.save_click) self._ui.actionLoad.triggered.connect(self.load_click) - # self._ui.actionExit.triggered.connect(self.exit_click) + self._ui.actionExit.triggered.connect(self.exit_click) def invalid_input_message(self, warning: str): """ @@ -133,7 +134,6 @@ def delete_all_class_card(self): if item is not None: class_card = item.widget() if isinstance(class_card, ClassCard): - print(i) self._grid_layout.removeWidget(class_card) class_card.deleteLater() self._size -= 1 # Decrement the total count of class cards @@ -174,6 +174,23 @@ def confirm_load_clicked(self): # Emit signal to controller to handle task self._process_task_signal.emit(task, self._dialog) + #exit + def exit_click(self): + """ + Opens a dialog for exit and connects confirm button + """ + msg = QMessageBox() + msg.setWindowTitle("Exit") + msg.setText("Are you sure you want to proceed?(Don't forget to save your digram:)") + msg.setIcon(QMessageBox.Icon.Question) + msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) + + result = msg.exec() + if result == QMessageBox.StandardButton.Yes: + sys.exit() + else: + pass + ################################################################################################## def enable_widgets(self, enabled: bool, active_widget: QWidget): From 4d3c7c6ee6847cc6a1d61136c7fbdb36003202fb Mon Sep 17 00:00:00 2001 From: Marshall Feng Date: Fri, 1 Mar 2024 18:26:42 -0500 Subject: [PATCH 126/144] Add gui display help information feature --- src/umleditor/mvc_view/gui_view/uml.ui | 6 ++++++ src/umleditor/mvc_view/gui_view/view_GUI.py | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/src/umleditor/mvc_view/gui_view/uml.ui b/src/umleditor/mvc_view/gui_view/uml.ui index a4888a87..b1d3efbd 100644 --- a/src/umleditor/mvc_view/gui_view/uml.ui +++ b/src/umleditor/mvc_view/gui_view/uml.ui @@ -49,6 +49,7 @@ + @@ -90,6 +91,11 @@ Exit + + + Help + + diff --git a/src/umleditor/mvc_view/gui_view/view_GUI.py b/src/umleditor/mvc_view/gui_view/view_GUI.py index dddc3d5f..a9505e37 100644 --- a/src/umleditor/mvc_view/gui_view/view_GUI.py +++ b/src/umleditor/mvc_view/gui_view/view_GUI.py @@ -48,6 +48,7 @@ def connect_menu(self): self._ui.actionSave.triggered.connect(self.save_click) self._ui.actionLoad.triggered.connect(self.load_click) self._ui.actionExit.triggered.connect(self.exit_click) + self._ui.actionHelp_2.triggered.connect(self.help_click) def invalid_input_message(self, warning: str): """ @@ -191,6 +192,26 @@ def exit_click(self): else: pass + #help + def help_click(self): + msg = QMessageBox() + msg.setWindowTitle("Help") + info = '' + info += "Right click class name to add field/method/relationship" + info += '\n' + info += "Right click to edit selected rows" + info += '\n' + info += "Press Esc to stop editing" + info += '\n' + info += "Unsaved rows will be deleted and saved rows will be returned to its original state" + info += '\n' + info += "Relationship Types: 'aggregation', 'composition', 'inheritance', 'realization'" + info += '\n' + msg.setText(info) + msg.setIcon(QMessageBox.Icon.Information) + msg.setStandardButtons(QMessageBox.StandardButton.Ok) + msg.exec() + ################################################################################################## def enable_widgets(self, enabled: bool, active_widget: QWidget): From 1f7564c7a89bb4de7a6e98f46eb890302e58a15f Mon Sep 17 00:00:00 2001 From: AdamG-L Date: Fri, 1 Mar 2024 19:08:51 -0500 Subject: [PATCH 127/144] Added rename functionality --- .../mvc_controller/gui_controller.py | 6 +++++ src/umleditor/mvc_view/gui_view/class_card.py | 25 ++++++++++++++++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/umleditor/mvc_controller/gui_controller.py b/src/umleditor/mvc_controller/gui_controller.py index 155b5dcb..356d5c73 100644 --- a/src/umleditor/mvc_controller/gui_controller.py +++ b/src/umleditor/mvc_controller/gui_controller.py @@ -54,6 +54,9 @@ def run(self, task: str, widget: QtWidgets): if 'load' in task: self.load_file(widget) return + if 'class -r' in task: + self.rename_class(task,widget) + return if "class -a" in task: self.add_class(task, widget) # No action required after deleting @@ -94,6 +97,9 @@ def load_file(self, widget: QtWidgets): s = relation._destination._name + ' ' + relation._type class_card.add_relation(s) + def rename_class(self, task: str, widget: QtWidgets): + words = task.split() + widget.accept_new_name(words[-1]) def add_class(self, task: str, widget: QtWidgets): """ diff --git a/src/umleditor/mvc_view/gui_view/class_card.py b/src/umleditor/mvc_view/gui_view/class_card.py index 2de4bedb..fdab5fbc 100644 --- a/src/umleditor/mvc_view/gui_view/class_card.py +++ b/src/umleditor/mvc_view/gui_view/class_card.py @@ -1,6 +1,7 @@ from PyQt6.QtWidgets import QWidget, QVBoxLayout, QListWidget, QMenu, QLineEdit, QLabel, QListWidgetItem from PyQt6.QtGui import QAction from PyQt6.QtCore import Qt, pyqtSignal, QEvent +from umleditor.mvc_view.gui_view.class_input_dialog import CustomInputDialog class ClassCard(QWidget): """ @@ -100,22 +101,25 @@ def show_class_menu(self, position): """ # Create menu & Actions menu = QMenu() + rename_action = QAction("Rename Class", self) delete_action = QAction("Delete Class", self) field_action = QAction("Add Field", self) method_action = QAction("Add Method", self) relation_action = QAction("Add Relation", self) - menu.addAction(delete_action) - menu.addSeparator() + menu.addAction(rename_action) menu.addAction(field_action) menu.addAction(method_action) menu.addAction(relation_action) - + menu.addSeparator() + menu.addAction(delete_action) # Add button functionality delete_action.triggered.connect(self.confirm_delete_class) field_action.triggered.connect(lambda: self.menu_action_clicked(self._list_field, "Enter Field")) method_action.triggered.connect(lambda: self.menu_action_clicked(self._list_method, "e.g. method param1 param2...")) relation_action.triggered.connect(lambda: self.menu_action_clicked(self._list_relation, "e.g. dst type")) + rename_action.triggered.connect(self.rename_action_clicked) + # Create Menu menu.exec(self.mapToGlobal(position)) @@ -130,6 +134,7 @@ def show_row_menu(self, position, widget: QLineEdit): menu = QMenu() edit_action = QAction("Edit", self) delete_action = QAction("Delete", self) + menu.addAction(edit_action) menu.addAction(delete_action) @@ -145,6 +150,20 @@ def show_row_menu(self, position, widget: QLineEdit): # Create Menu menu.exec(global_position) + + def rename_action_clicked(self): + self._rename_dialog = CustomInputDialog(name="Rename Class") + self._rename_dialog.ok_button.clicked.connect(self.confirm_rename_clicked) + self._rename_dialog.exec() + + def confirm_rename_clicked(self): + self._process_task_signal.emit("class -r " + self._name + " " + self._rename_dialog.input_text.text(), self) + + def accept_new_name(self, new_name: str): + self._class_label.setText(new_name) + self.set_name(new_name) + self._rename_dialog.reject() + def edit_action_clicked(self, widget: QLineEdit): """ From bebe68e2af01761c2180c8da320b1cd646243f3b Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 2 Mar 2024 19:19:33 -0500 Subject: [PATCH 128/144] Adding Tests --- src/test/test_imports.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/test/test_imports.py b/src/test/test_imports.py index 1b51f56a..cdaded22 100644 --- a/src/test/test_imports.py +++ b/src/test/test_imports.py @@ -4,6 +4,7 @@ def test_import_cli_controller(): from umleditor.mvc_controller.cli_controller import CLI_Controller assert CLI_Controller + assert CLI_Controller.run def test_import_controller_input(): from umleditor.mvc_controller.controller_input import read_line, read_file @@ -18,10 +19,21 @@ def test_import_controller_output(): def test_import_controller(): from umleditor.mvc_controller import Controller assert Controller + assert Controller.run + assert Controller.quit + assert Controller.save + assert Controller.load def test_import_gui_controller(): from umleditor.mvc_controller.gui_controller import ControllerGUI assert ControllerGUI + assert ControllerGUI.run + assert ControllerGUI.save_file + assert ControllerGUI.load_file + assert ControllerGUI.rename_class + assert ControllerGUI.add_class + assert ControllerGUI.delete_class + assert ControllerGUI.acceptance_state def test_import_serialzer(): from umleditor.mvc_controller.serializer import CustomJSONEncoder, serialize, deserialize @@ -50,10 +62,37 @@ def test_import_custom_exceptions(): def test_import_diagram(): from umleditor.mvc_model import Diagram assert Diagram + assert Diagram.add_entity + assert Diagram.get_entity + assert Diagram.delete_entity + assert Diagram.has_entity + assert Diagram.rename_entity + assert Diagram.list_everything + assert Diagram.list_entity_details + assert Diagram.list_entities + assert Diagram.list_relations + assert Diagram.list_entity_relations + assert Diagram.add_relation + assert Diagram.delete_relation + assert Diagram.change_relation_type + assert Diagram.edit_relation def test_import_entity(): from umleditor.mvc_model import Entity assert Entity + assert Entity.get_name + assert Entity.set_name + assert Entity.add_field + assert Entity.delete_field + assert Entity.rename_field + assert Entity.get_method + assert Entity.add_method + assert Entity.add_method_and_params + assert Entity.edit_method + assert Entity.delete_method + assert Entity.rename_method + assert Entity.list_fields + assert Entity.list_methods def test_import_help(): from umleditor.mvc_model.help_command import help_menu From da23ee74a8cb327f7f42d5e9890eab9a0d9e5028 Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 2 Mar 2024 19:25:33 -0500 Subject: [PATCH 129/144] Test_imports Model test updates --- src/test/test_imports.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/test/test_imports.py b/src/test/test_imports.py index cdaded22..99bf663b 100644 --- a/src/test/test_imports.py +++ b/src/test/test_imports.py @@ -94,18 +94,26 @@ def test_import_entity(): assert Entity.list_fields assert Entity.list_methods +def test_import_uml_method(): + from umleditor.mvc_model.entity import UML_Method + assert UML_Method + assert UML_Method.get_method_name + assert UML_Method.set_method_name + assert UML_Method.add_parameters + assert UML_Method.remove_parameters + assert UML_Method.change_parameters + def test_import_help(): from umleditor.mvc_model.help_command import help_menu assert help_menu -def test_import_method(): - from umleditor.mvc_model import UML_Method - assert UML_Method - def test_import_relation(): from umleditor.mvc_model import Relation assert Relation - + assert Relation.get_source + assert Relation.get_destination + assert Relation.set_type + assert Relation.contains #===============================================================================# #Import From View Tests# From bec0678a375baaa61a90e616ad5de575871a6d1e Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 2 Mar 2024 19:38:00 -0500 Subject: [PATCH 130/144] Finished rounding out test_imports. --- src/test/test_imports.py | 44 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/src/test/test_imports.py b/src/test/test_imports.py index 99bf663b..45622945 100644 --- a/src/test/test_imports.py +++ b/src/test/test_imports.py @@ -121,6 +121,31 @@ def test_import_relation(): def test_import_class_card(): from umleditor.mvc_view.gui_view.class_card import ClassCard assert ClassCard + assert ClassCard.set_name + assert ClassCard.initUI + assert ClassCard.connect_menus + assert ClassCard.set_styles + assert ClassCard.show_class_menu + assert ClassCard.show_row_menu + assert ClassCard.rename_action_clicked + assert ClassCard.confirm_rename_clicked + assert ClassCard.accept_new_name + assert ClassCard.edit_action_clicked + assert ClassCard.delete_action_clicked + assert ClassCard.confirm_delete_class + assert ClassCard.menu_action_clicked + assert ClassCard.eventFilter + assert ClassCard.escape_from_row + assert ClassCard.enable_context_menus + assert ClassCard.verify_input + assert ClassCard.split_relation + assert ClassCard.deselect_line + assert ClassCard.get_selected_line + assert ClassCard.get_task_signal + assert ClassCard.get_enable_signal + assert ClassCard.add_field + assert ClassCard.add_method + assert ClassCard.add_relation def test_import_class_input_dialog(): from umleditor.mvc_view.gui_view.class_input_dialog import CustomInputDialog @@ -128,4 +153,21 @@ def test_import_class_input_dialog(): def test_import_view_gui(): from umleditor.mvc_view.gui_view.view_GUI import ViewGUI - assert ViewGUI \ No newline at end of file + assert ViewGUI + assert ViewGUI.get_signal + assert ViewGUI.connect_menu + assert ViewGUI.invalid_input_message + assert ViewGUI.forward_signal + assert ViewGUI.add_class_click + assert ViewGUI.confirm_class_clicked + assert ViewGUI.add_class_card + assert ViewGUI.delete_class_card + assert ViewGUI.delete_all_class_card + assert ViewGUI.save_click + assert ViewGUI.confirm_save_clicked + assert ViewGUI.load_click + assert ViewGUI.confirm_load_clicked + assert ViewGUI.exit_click + assert ViewGUI.help_click + assert ViewGUI.enable_widgets + \ No newline at end of file From 90c57d214d6c1a0430a88b62a54c365c44c00d54 Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 2 Mar 2024 19:53:37 -0500 Subject: [PATCH 131/144] Updated Test_diagram --- src/test/test_diagram.py | 52 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/src/test/test_diagram.py b/src/test/test_diagram.py index 2a1548eb..4b498873 100644 --- a/src/test/test_diagram.py +++ b/src/test/test_diagram.py @@ -10,8 +10,24 @@ def test_create_diagram(): def test_dia_add_entity(): dia = Diagram() + assert not dia.has_entity("ent") dia.add_entity("ent") - assert dia._entities + assert dia.has_entity("ent") + +def test_dia_add_multiple_entities(): + dia = Diagram() + assert not dia.has_entity("ent1") + assert not dia.has_entity("ent2") + assert not dia.has_entity("ent3") + dia.add_entity("ent1") + dia.add_entity("ent2") + assert dia.has_entity("ent1") + assert dia.has_entity("ent2") + assert not dia.has_entity("ent3") + dia.add_entity("ent3") + assert dia.has_entity("ent1") + assert dia.has_entity("ent2") + assert dia.has_entity("ent3") def test_dia_get_entity(): dia = Diagram() @@ -31,6 +47,24 @@ def test_dia_delete_entity(): dia.delete_entity("ent1") assert not dia.has_entity("ent1") +def test_dia_delete_multiple_entities(): + dia = Diagram() + dia.add_entity("ent1") + dia.add_entity("ent2") + dia.add_entity("ent3") + assert dia.has_entity("ent1") + assert dia.has_entity("ent2") + assert dia.has_entity("ent3") + dia.delete_entity("ent1") + dia.delete_entity("ent2") + assert not dia.has_entity("ent1") + assert not dia.has_entity("ent2") + assert dia.has_entity("ent3") + dia.delete_entity("ent3") + assert not dia.has_entity("ent1") + assert not dia.has_entity("ent2") + assert not dia.has_entity("ent3") + def test_dia_rename_entity(): dia = Diagram() dia.add_entity("ent1") @@ -40,6 +74,22 @@ def test_dia_rename_entity(): assert not dia.has_entity("ent1") assert dia.has_entity("ent2") +def test_dia_rename_multiple_entities(): + dia = Diagram() + dia.add_entity("ent1") + dia.add_entity("ent2") + assert dia.has_entity("ent1") + assert dia.has_entity("ent2") + assert not dia.has_entity("ent3") + assert not dia.has_entity("ent4") + dia.rename_entity("ent1", "ent3") + dia.rename_entity("ent2", "ent4") + assert not dia.has_entity("ent1") + assert not dia.has_entity("ent2") + assert dia.has_entity("ent3") + assert dia.has_entity("ent4") + + def test_dia_add_relation(): dia = Diagram() dia.add_entity("ent1") From 3f2d451948c9441cc28960c8361910167ae50ef6 Mon Sep 17 00:00:00 2001 From: Marshall Feng Date: Sat, 2 Mar 2024 22:41:04 -0500 Subject: [PATCH 132/144] Simplify loading relation's source/destination --- src/umleditor/mvc_controller/serializer.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/umleditor/mvc_controller/serializer.py b/src/umleditor/mvc_controller/serializer.py index 820b1f32..d326cc03 100644 --- a/src/umleditor/mvc_controller/serializer.py +++ b/src/umleditor/mvc_controller/serializer.py @@ -144,11 +144,13 @@ def deserialize(diagram: Diagram, path: str) -> None: for saved_relationship in obj['relationships']: loaded_relationship = Relation() # relationship source - # loaded_relationship._source = saved_relationship['source'] - loaded_relationship._source = [entity for entity in loaded_classes if entity._name == saved_relationship['source']][0] + for entity in loaded_classes: + if entity._name == saved_relationship['source']: + loaded_relationship._source = entity # relationship destination - # loaded_relationship._destination = saved_relationship['destination'] - loaded_relationship._destination = [entity for entity in loaded_classes if entity._name == saved_relationship['destination']][0] + for entity in loaded_classes: + if entity._name == saved_relationship['destination']: + loaded_relationship._destination = entity # relationship type loaded_relationship._type = saved_relationship['type'] loaded_relationships.append(loaded_relationship) From 581b9a399ca3a471084d374027066cbe3e6381f0 Mon Sep 17 00:00:00 2001 From: Marshall Feng Date: Sun, 3 Mar 2024 14:50:40 -0500 Subject: [PATCH 133/144] Update __eq__ of Relation to check if types are equal --- src/umleditor/mvc_model/relation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/umleditor/mvc_model/relation.py b/src/umleditor/mvc_model/relation.py index 9eb190a7..61c1eaea 100644 --- a/src/umleditor/mvc_model/relation.py +++ b/src/umleditor/mvc_model/relation.py @@ -108,4 +108,4 @@ def __eq__(self, other): if self._destination != other._destination: return False - return True \ No newline at end of file + return self._type == other._type \ No newline at end of file From 06a465f137484cd82e0ccdd0315050a88f2690dc Mon Sep 17 00:00:00 2001 From: Marshall Feng Date: Sun, 3 Mar 2024 14:53:18 -0500 Subject: [PATCH 134/144] Remove uneeded import --- src/umleditor/mvc_controller/controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/umleditor/mvc_controller/controller.py b/src/umleditor/mvc_controller/controller.py index e66dcd01..1a31acb1 100644 --- a/src/umleditor/mvc_controller/controller.py +++ b/src/umleditor/mvc_controller/controller.py @@ -1,6 +1,6 @@ from .controller_input import read_line import umleditor.mvc_controller.controller_output as controller_output -from .serializer import CustomJSONEncoder, serialize, deserialize +from .serializer import serialize, deserialize from umleditor.mvc_controller.uml_parser import parse from umleditor.mvc_model import CustomExceptions as CE from umleditor.mvc_model.diagram import Diagram From 28ac0ae2ed3dcc4bf3e43671e1409d7525673fa7 Mon Sep 17 00:00:00 2001 From: Marshall Feng Date: Sun, 3 Mar 2024 15:20:14 -0500 Subject: [PATCH 135/144] Fix bugs of storing all parameters as one list instead of list of parameters --- src/umleditor/mvc_model/entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/umleditor/mvc_model/entity.py b/src/umleditor/mvc_model/entity.py index b1f32986..c3bebca7 100644 --- a/src/umleditor/mvc_model/entity.py +++ b/src/umleditor/mvc_model/entity.py @@ -141,7 +141,7 @@ def add_method_and_params(self, method_name: str, *params) : *params: Variable-length argument list representing the parameters for the method. """ self.add_method(method_name) - self.get_method(method_name).add_parameters([params]) + self.get_method(method_name).add_parameters(list(params)) def edit_method(self, old_method: str, new_method: str, *params): deleted_method = self.get_method(old_method) From ea452d8afd51c6f5648f7111f8e943b8be4498f8 Mon Sep 17 00:00:00 2001 From: Marshall Feng Date: Sun, 3 Mar 2024 15:22:45 -0500 Subject: [PATCH 136/144] Add tests for serializer --- src/test/test_serializer.py | 147 ++++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 src/test/test_serializer.py diff --git a/src/test/test_serializer.py b/src/test/test_serializer.py new file mode 100644 index 00000000..29217b41 --- /dev/null +++ b/src/test/test_serializer.py @@ -0,0 +1,147 @@ +import os +from umleditor.mvc_controller.serializer import serialize, deserialize +from umleditor.mvc_model.diagram import Diagram +from umleditor.mvc_model.entity import Entity +from umleditor.mvc_model.relation import Relation + +path = os.path.join(os.path.dirname(__file__), '../', '../', 'save') +if not os.path.exists(path): + os.makedirs(path) +path = os.path.join(path, 'test' + '.json') + +def test_empty(): + dia = Diagram() + serialize(dia, path) + + load_dia = Diagram() + load_dia.add_entity('Entity1') + load_dia.add_entity('Entity2') + load_dia.add_relation('Entity1', 'Entity2', 'composition') + load_dia.add_relation('Entity2', 'Entity1', 'realization') + load_dia.get_entity('Entity1').add_field('Field1') + load_dia.get_entity('Entity2').add_field('Field1') + load_dia.get_entity('Entity2').add_field('Field2') + load_dia.get_entity('Entity1').add_method_and_params('Method1') + load_dia.get_entity('Entity1').add_method_and_params('Method2') + load_dia.get_entity('Entity2').add_method_and_params('Method1', 'Param1') + deserialize(load_dia, path) + assert not load_dia.has_entity('Entity1') + assert not load_dia.has_entity('Entity2') + +def test_entities(): + dia = Diagram() + dia.add_entity('Class1') + dia.add_entity('Class2') + serialize(dia, path) + + load_dia = Diagram() + load_dia.add_entity('Entity1') + load_dia.add_entity('Entity2') + load_dia.add_relation('Entity1', 'Entity2', 'composition') + load_dia.add_relation('Entity2', 'Entity1', 'realization') + load_dia.get_entity('Entity1').add_field('Field1') + load_dia.get_entity('Entity2').add_field('Field1') + load_dia.get_entity('Entity2').add_field('Field2') + load_dia.get_entity('Entity1').add_method_and_params('Method1') + load_dia.get_entity('Entity1').add_method_and_params('Method2') + load_dia.get_entity('Entity2').add_method_and_params('Method1', 'Param1') + deserialize(load_dia, path) + assert not load_dia.has_entity('Entity1') + assert not load_dia.has_entity('Entity2') + + assert load_dia.has_entity('Class1') + assert load_dia.has_entity('Class2') + +def test_relations(): + dia = Diagram() + dia.add_entity('Class1') + dia.add_entity('Class2') + dia.add_relation('Class1', 'Class2', 'composition') + dia.add_relation('Class2', 'Class1', 'inheritance') + serialize(dia, path) + + load_dia = Diagram() + load_dia.add_entity('Entity1') + load_dia.add_entity('Entity2') + load_dia.add_relation('Entity1', 'Entity2', 'composition') + load_dia.add_relation('Entity2', 'Entity1', 'realization') + load_dia.get_entity('Entity1').add_field('Field1') + load_dia.get_entity('Entity2').add_field('Field1') + load_dia.get_entity('Entity2').add_field('Field2') + load_dia.get_entity('Entity1').add_method_and_params('Method1') + load_dia.get_entity('Entity1').add_method_and_params('Method2') + load_dia.get_entity('Entity2').add_method_and_params('Method1', 'Param1') + deserialize(load_dia, path) + assert not load_dia.has_entity('Entity1') + assert not load_dia.has_entity('Entity2') + + assert load_dia.has_entity('Class1') + assert load_dia.has_entity('Class2') + assert Relation('composition', Entity('Class1'), Entity('Class2')) in load_dia._relations + assert Relation('inheritance', Entity('Class2'), Entity('Class1')) in load_dia._relations + +def test_fields(): + dia = Diagram() + dia.add_entity('Class1') + dia.add_entity('Class2') + dia.get_entity('Class1').add_field('Field1') + dia.get_entity('Class1').add_field('Field2') + dia.get_entity('Class2').add_field('Field1') + serialize(dia, path) + + load_dia = Diagram() + load_dia.add_entity('Entity1') + load_dia.add_entity('Entity2') + load_dia.add_relation('Entity1', 'Entity2', 'composition') + load_dia.add_relation('Entity2', 'Entity1', 'realization') + load_dia.get_entity('Entity1').add_field('Field1') + load_dia.get_entity('Entity2').add_field('Field1') + load_dia.get_entity('Entity2').add_field('Field2') + load_dia.get_entity('Entity1').add_method_and_params('Method1') + load_dia.get_entity('Entity1').add_method_and_params('Method2') + load_dia.get_entity('Entity2').add_method_and_params('Method1', 'Param1') + deserialize(load_dia, path) + assert not load_dia.has_entity('Entity1') + assert not load_dia.has_entity('Entity2') + + assert load_dia.has_entity('Class1') + assert load_dia.has_entity('Class2') + assert 'Field1' in load_dia.get_entity('Class1')._fields + assert 'Field2' in load_dia.get_entity('Class1')._fields + assert 'Field1' in load_dia.get_entity('Class2')._fields + +def test_methods(): + dia = Diagram() + dia.add_entity('Class1') + dia.add_entity('Class2') + dia.get_entity('Class1').add_method_and_params('Method1') + dia.get_entity('Class2').add_method_and_params('Method1', 'Param1') + dia.get_entity('Class2').add_method_and_params('Method2') + dia.get_entity('Class2').add_method_and_params('Method3', 'Param1', 'Param2', 'Param3') + serialize(dia, path) + + load_dia = Diagram() + load_dia.add_entity('Entity1') + load_dia.add_entity('Entity2') + load_dia.add_relation('Entity1', 'Entity2', 'composition') + load_dia.add_relation('Entity2', 'Entity1', 'realization') + load_dia.get_entity('Entity1').add_field('Field1') + load_dia.get_entity('Entity2').add_field('Field1') + load_dia.get_entity('Entity2').add_field('Field2') + load_dia.get_entity('Entity1').add_method_and_params('Method1') + load_dia.get_entity('Entity1').add_method_and_params('Method2') + load_dia.get_entity('Entity2').add_method_and_params('Method1', 'Param1') + deserialize(load_dia, path) + assert not load_dia.has_entity('Entity1') + assert not load_dia.has_entity('Entity2') + + assert load_dia.has_entity('Class1') + assert load_dia.has_entity('Class2') + assert load_dia.get_entity('Class1').get_method('Method1') + assert load_dia.get_entity('Class2').get_method('Method1') + assert 'Param1' in load_dia.get_entity('Class2').get_method('Method1')._params + assert load_dia.get_entity('Class2').get_method('Method2') + assert load_dia.get_entity('Class2').get_method('Method3') + assert 'Param1' in load_dia.get_entity('Class2').get_method('Method3')._params + assert 'Param2' in load_dia.get_entity('Class2').get_method('Method3')._params + assert 'Param3' in load_dia.get_entity('Class2').get_method('Method3')._params \ No newline at end of file From b64a01425d06bdaef04b4dbc9e75d05a2b8b2b37 Mon Sep 17 00:00:00 2001 From: Marshall Feng Date: Sun, 3 Mar 2024 16:45:32 -0500 Subject: [PATCH 137/144] Add more tests for serializer --- src/test/test_serializer.py | 46 +++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/test/test_serializer.py b/src/test/test_serializer.py index 29217b41..be7c8a99 100644 --- a/src/test/test_serializer.py +++ b/src/test/test_serializer.py @@ -144,4 +144,50 @@ def test_methods(): assert load_dia.get_entity('Class2').get_method('Method3') assert 'Param1' in load_dia.get_entity('Class2').get_method('Method3')._params assert 'Param2' in load_dia.get_entity('Class2').get_method('Method3')._params + assert 'Param3' in load_dia.get_entity('Class2').get_method('Method3')._params + +def test_all_together(): + dia = Diagram() + dia.add_entity('Class1') + dia.add_entity('Class2') + dia.add_relation('Class1', 'Class2', 'composition') + dia.add_relation('Class2', 'Class1', 'inheritance') + dia.get_entity('Class1').add_field('Field1') + dia.get_entity('Class1').add_field('Field2') + dia.get_entity('Class2').add_field('Field1') + dia.get_entity('Class1').add_method_and_params('Method1') + dia.get_entity('Class2').add_method_and_params('Method1', 'Param1') + dia.get_entity('Class2').add_method_and_params('Method2') + dia.get_entity('Class2').add_method_and_params('Method3', 'Param1', 'Param2', 'Param3') + serialize(dia, path) + + load_dia = Diagram() + load_dia.add_entity('Entity1') + load_dia.add_entity('Entity2') + load_dia.add_relation('Entity1', 'Entity2', 'composition') + load_dia.add_relation('Entity2', 'Entity1', 'realization') + load_dia.get_entity('Entity1').add_field('Field1') + load_dia.get_entity('Entity2').add_field('Field1') + load_dia.get_entity('Entity2').add_field('Field2') + load_dia.get_entity('Entity1').add_method_and_params('Method1') + load_dia.get_entity('Entity1').add_method_and_params('Method2') + load_dia.get_entity('Entity2').add_method_and_params('Method1', 'Param1') + deserialize(load_dia, path) + assert not load_dia.has_entity('Entity1') + assert not load_dia.has_entity('Entity2') + + assert load_dia.has_entity('Class1') + assert load_dia.has_entity('Class2') + assert Relation('composition', Entity('Class1'), Entity('Class2')) in load_dia._relations + assert Relation('inheritance', Entity('Class2'), Entity('Class1')) in load_dia._relations + assert 'Field1' in load_dia.get_entity('Class1')._fields + assert 'Field2' in load_dia.get_entity('Class1')._fields + assert 'Field1' in load_dia.get_entity('Class2')._fields + assert load_dia.get_entity('Class1').get_method('Method1') + assert load_dia.get_entity('Class2').get_method('Method1') + assert 'Param1' in load_dia.get_entity('Class2').get_method('Method1')._params + assert load_dia.get_entity('Class2').get_method('Method2') + assert load_dia.get_entity('Class2').get_method('Method3') + assert 'Param1' in load_dia.get_entity('Class2').get_method('Method3')._params + assert 'Param2' in load_dia.get_entity('Class2').get_method('Method3')._params assert 'Param3' in load_dia.get_entity('Class2').get_method('Method3')._params \ No newline at end of file From d7fcb1d9fea2fd50eb206818a6382e5e4469d97e Mon Sep 17 00:00:00 2001 From: Marshall Feng Date: Sun, 3 Mar 2024 17:00:03 -0500 Subject: [PATCH 138/144] cleanup serialize function --- src/umleditor/mvc_controller/serializer.py | 31 +++++++++++----------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/umleditor/mvc_controller/serializer.py b/src/umleditor/mvc_controller/serializer.py index d326cc03..44caf9a8 100644 --- a/src/umleditor/mvc_controller/serializer.py +++ b/src/umleditor/mvc_controller/serializer.py @@ -31,56 +31,55 @@ def serialize(diagram: Diagram, path: str) -> None: # classes saved_classes = [] for entity in diagram._entities: - saved_class = {} + # class + saved_class = {}; saved_classes.append(saved_class) # class name saved_class['name'] = entity._name # class fields - saved_fields = [] + saved_fields = []; saved_class['fields'] = saved_fields for field in entity._fields: - saved_field = {} + # class field + saved_field = {}; saved_fields.append(saved_field) # class field name saved_field['name'] = field # class field type saved_field['type'] = 'undefined' #TODO - saved_fields.append(saved_field) - saved_class['fields'] = saved_fields # class methods - saved_methods = [] + saved_methods = []; saved_class['methods'] = saved_methods for method in entity._methods: - saved_method = {} + # class method + saved_method = {}; saved_methods.append(saved_method) # class method name saved_method['name'] = method._name # class method return_type saved_method['return_type'] = 'undefined' #TODO # class method params - saved_params = [] + saved_params = []; saved_method['params'] = saved_params for param in method._params: - saved_param = {} + # class method param + saved_param = {}; saved_params.append(saved_param) # class method param name saved_param['name'] = param # class method param type saved_param['type'] = 'undefined' # TODO - saved_params.append(saved_param) - saved_method['params'] = saved_params - saved_methods.append(saved_method) - saved_class['methods'] = saved_methods - saved_classes.append(saved_class) # relationships saved_relationships = [] for relation in diagram._relations: - saved_relationship = {} + # relationship + saved_relationship = {}; saved_relationships.append(saved_relationship) # relationship source saved_relationship['source'] = relation._source._name # relationship destination saved_relationship['destination'] = relation._destination._name # relationship type saved_relationship['type'] = relation._type - saved_relationships.append(saved_relationship) + try: obj = {'classes': saved_classes, 'relationships': saved_relationships} content = json.dumps(obj=obj, cls=CustomJSONEncoder) except Exception: raise CE.JsonEncodeError(filepath=path) + controller_output.write_file(path=path, content=content) def deserialize(diagram: Diagram, path: str) -> None: From 7e19c20c7323c9cd75d185de6c70e62c1fd6d74b Mon Sep 17 00:00:00 2001 From: Marshall Feng Date: Sun, 3 Mar 2024 17:10:59 -0500 Subject: [PATCH 139/144] Cleanup deserialize function(can't make it as clean as serialize function due to str() type field/param) --- src/umleditor/mvc_controller/serializer.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/umleditor/mvc_controller/serializer.py b/src/umleditor/mvc_controller/serializer.py index 44caf9a8..cc84b486 100644 --- a/src/umleditor/mvc_controller/serializer.py +++ b/src/umleditor/mvc_controller/serializer.py @@ -95,20 +95,24 @@ def deserialize(diagram: Diagram, path: str) -> None: - (CustomExceptions.SavedDataError): If file data is not consistent with the Diagram ''' content = read_file(path) + try: obj = json.loads(content) except Exception: raise CE.JsonDecodeError(filepath=path) + try: # classes loaded_classes = [] for saved_class in obj['classes']: + # class loaded_class = Entity() # class name loaded_class._name = saved_class['name'] # class fields loaded_fields = [] for saved_field in saved_class['fields']: + # class field loaded_field = str() # str is the type of field # class field name loaded_field = saved_field['name'] @@ -119,6 +123,7 @@ def deserialize(diagram: Diagram, path: str) -> None: # class methods loaded_methods = [] for saved_method in saved_class['methods']: + # class method loaded_method = UML_Method() # class method name loaded_method._name = saved_method['name'] @@ -127,6 +132,7 @@ def deserialize(diagram: Diagram, path: str) -> None: # class method params loaded_params = [] for saved_param in saved_method['params']: + # class method param loaded_param = str() # str is the type of param # class method param name loaded_param = saved_param['name'] @@ -141,6 +147,7 @@ def deserialize(diagram: Diagram, path: str) -> None: # relationships loaded_relationships = [] for saved_relationship in obj['relationships']: + # relationship loaded_relationship = Relation() # relationship source for entity in loaded_classes: From aad36be8eef3c751dc4d5fca9cec2be2ed423b45 Mon Sep 17 00:00:00 2001 From: Marshall Feng Date: Sun, 3 Mar 2024 17:58:14 -0500 Subject: [PATCH 140/144] Fix Relations equivalence check in add_relation --- src/umleditor/mvc_model/diagram.py | 2 +- src/umleditor/mvc_model/relation.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/umleditor/mvc_model/diagram.py b/src/umleditor/mvc_model/diagram.py index ba484fa8..70204246 100644 --- a/src/umleditor/mvc_model/diagram.py +++ b/src/umleditor/mvc_model/diagram.py @@ -212,7 +212,7 @@ def add_relation(self,source:str, destination:str, type:str): to_add = Relation(type, src, dst) for rel in self._relations: - if rel == to_add: + if rel.equal_without_type(to_add): raise CustomExceptions.RelationExistsError(source, destination) # Pass entity objects to relation and add relation to list of existing relations self._relations.append(to_add) diff --git a/src/umleditor/mvc_model/relation.py b/src/umleditor/mvc_model/relation.py index 61c1eaea..779e33ee 100644 --- a/src/umleditor/mvc_model/relation.py +++ b/src/umleditor/mvc_model/relation.py @@ -79,6 +79,20 @@ def contains(self, name:str): return True else: return False + + def equal_without_type(self, other): + ''' + Checks if equal to other Relation without checking type equivalence. + + Return: + True if all fields except for type are equivalent, false otherwise. + ''' + if self._source != other._source: + return False + + if self._destination != other._destination: + return False + return True def __str__(self): """ From d810e59b13daaf2fef13d1db62557f077251da51 Mon Sep 17 00:00:00 2001 From: Marshall Feng Date: Sun, 3 Mar 2024 18:23:55 -0500 Subject: [PATCH 141/144] Fix bugs of add_paramter change state of diagram even if failed --- src/umleditor/mvc_model/entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/umleditor/mvc_model/entity.py b/src/umleditor/mvc_model/entity.py index b1f32986..e74c4983 100644 --- a/src/umleditor/mvc_model/entity.py +++ b/src/umleditor/mvc_model/entity.py @@ -298,7 +298,7 @@ def add_parameters(self, params: list[str]): for new_param in params: if new_param in self._params: raise CustomExceptions.ParameterExistsError(new_param) - self._params.append(new_param) + self._params.extend(params) def remove_parameters(self, params: list[str]): """ From f63d8d79ff7bfbb5cd8d730de2d9630641630b2d Mon Sep 17 00:00:00 2001 From: Marshall Feng Date: Sun, 3 Mar 2024 18:39:39 -0500 Subject: [PATCH 142/144] Add duplicate checks for parameter operations --- src/umleditor/mvc_model/custom_exceptions.py | 10 ++++++++++ src/umleditor/mvc_model/entity.py | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/umleditor/mvc_model/custom_exceptions.py b/src/umleditor/mvc_model/custom_exceptions.py index eeb5d063..89307fc5 100644 --- a/src/umleditor/mvc_model/custom_exceptions.py +++ b/src/umleditor/mvc_model/custom_exceptions.py @@ -146,6 +146,16 @@ class ParameterNotFoundError(Error): def __init__(self, parameter_name): super().__init__(f"Parameter named: '{parameter_name}' does not exist.") + class DuplicateParametersError(Error): + """ + Exception raised when the parameter occurs more than once. + + Args: + parameter_name (str): The name of the parameter occurs more than once. + """ + def __init__(self, parameter_name): + super().__init__(f"Parameter named: '{parameter_name}' occurs more than once.") + #===============================================================================# #Parser/Controller Exceptions #===============================================================================# diff --git a/src/umleditor/mvc_model/entity.py b/src/umleditor/mvc_model/entity.py index c838ec01..a71e2858 100644 --- a/src/umleditor/mvc_model/entity.py +++ b/src/umleditor/mvc_model/entity.py @@ -282,6 +282,20 @@ def set_method_name(self, new_name): """ self._name = new_name + def _check_duplicate_paramters(self, params: list[str]): + """ + Checks if there are duplicate paramters. + + Args: + params (list[str]): The list of new parameters to be checked. + + Raises: + CustomExceptions.DuplicateParametersError: If any of the parameter occurs more than once. + """ + for param in params: + if params.count(param) != 1: + raise CustomExceptions.DuplicateParametersError(param) + def add_parameters(self, params: list[str]): """ Adds a list of new parameters to the method. @@ -295,6 +309,7 @@ def add_parameters(self, params: list[str]): Returns: None. """ + self._check_duplicate_paramters(params) for new_param in params: if new_param in self._params: raise CustomExceptions.ParameterExistsError(new_param) @@ -313,6 +328,7 @@ def remove_parameters(self, params: list[str]): Returns: None. """ + self._check_duplicate_paramters(params) for remove_param in params: if remove_param not in self._params: raise CustomExceptions.ParameterNotFoundError(remove_param) @@ -334,6 +350,8 @@ def change_parameters(self, old_params: list[str], new_params: list[str]): Returns: None. """ + self._check_duplicate_paramters(old_params) + self._check_duplicate_paramters(new_params) for remove_param in old_params: if remove_param not in self._params: raise CustomExceptions.ParameterNotFoundError(remove_param) From 0f9ccc3576a10d6113ca4345b00ee43dc0eb2bb2 Mon Sep 17 00:00:00 2001 From: Marshall Feng Date: Sun, 3 Mar 2024 19:18:20 -0500 Subject: [PATCH 143/144] Cleanup main.py and update readme --- README.md | 6 +++--- main.py | 11 ++++------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 07980f0b..76cc2b8d 100644 --- a/README.md +++ b/README.md @@ -60,9 +60,9 @@ Regardless of operating system, this project will install dependencies when it i [Linux Terminal Navigation](https://www.linode.com/docs/guides/linux-navigation-commands/) ### Operation modes -'py main.py' - default operation mode, opens a GUI. -'py main.py cli' - runs the program in CLI mode instead of creating a gui -'py main.py -O' - runs the program in CLI debug mode. This mode is nearly identical to the CLI mode, just with slightly less error handling. Use at your own risk. +- `'py main.py' - default operation mode, opens a GUI. +- `'py main.py cli' - runs the program in CLI mode instead of creating a gui. +- `'py main.py debug' - runs the program in CLI debug mode. This mode is nearly identical to the CLI mode, just with slightly less error handling. Use at your own risk. **If you are in the CLI mode, type 'help' for a list of commands.** **In the gui, use the menu options available at the top of the screen and/or by right clicking to manipulate the diagram to your needs** diff --git a/main.py b/main.py index c573ac89..cfa1f22b 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,3 @@ -from umleditor.mvc_controller import Controller from umleditor.mvc_controller.cli_controller import CLI_Controller from umleditor.mvc_view.gui_view.view_GUI import ViewGUI from umleditor.mvc_controller.gui_controller import ControllerGUI @@ -8,11 +7,9 @@ def main(): #decides which main to run - which_main = sys.argv[1] if len(sys.argv) > 1 else None - - if which_main == 'cli': + if len(sys.argv) == 2 and sys.argv[1] == 'cli': mainCLI() - elif which_main == '-O': + elif len(sys.argv) == 2 and sys.argv[1] == 'debug': debug_main() else: mainGUI() @@ -33,7 +30,7 @@ def mainCLI(): except EOFError: # This handles ctrl+D pass - except Exception as e: + except Exception: # Never expect errors to be caught here print('Oh no! Unexpected Error!') @@ -55,7 +52,7 @@ def mainGUI(): except EOFError: # This handles ctrl+D pass - except Exception as e: + except Exception: # Never expect errors to be caught here print('Oh no! Unexpected Error!') From c6177ba3ca9d84d36e9951ea7e1d4d56c980da2f Mon Sep 17 00:00:00 2001 From: Marshall Feng Date: Sun, 3 Mar 2024 19:24:47 -0500 Subject: [PATCH 144/144] Update operation option constrains --- main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index cfa1f22b..5bc85464 100644 --- a/main.py +++ b/main.py @@ -7,9 +7,9 @@ def main(): #decides which main to run - if len(sys.argv) == 2 and sys.argv[1] == 'cli': + if len(sys.argv) >= 2 and sys.argv[1] == 'cli': mainCLI() - elif len(sys.argv) == 2 and sys.argv[1] == 'debug': + elif len(sys.argv) >= 2 and sys.argv[1] == 'debug': debug_main() else: mainGUI()