diff --git a/ontouml_py/model/modelelement.py b/ontouml_py/model/modelelement.py index 69d95de..56b2b30 100644 --- a/ontouml_py/model/modelelement.py +++ b/ontouml_py/model/modelelement.py @@ -3,6 +3,7 @@ from abc import abstractmethod from typing import Any +from icecream import ic from pydantic import Field from ontouml_py.model.namedelement import NamedElement @@ -30,7 +31,7 @@ class ModelElement(NamedElement, ProjectElement): } @abstractmethod - def __init__(self, **data: dict[str, Any]) -> None: + def __init__(self, project, **data: dict[str, Any]) -> None: """Initialize a new ModelElement instance. :param data: Fields to be set on the model instance. This includes fields inherited from NamedElement and @@ -38,6 +39,10 @@ def __init__(self, **data: dict[str, Any]) -> None: :type data: Dict[str, Any] :raises ValueError: If the instance does not belong to the allowed subclasses. """ + ic() + ic(project, data) + NamedElement.__init__(self, **data) + ProjectElement.__init__(self, project, **data) self._validate_subclasses( [ "Decoratable", @@ -50,4 +55,3 @@ def __init__(self, **data: dict[str, Any]) -> None: "Packageable", ], ) - super().__init__(**data) diff --git a/ontouml_py/model/namedelement.py b/ontouml_py/model/namedelement.py index eb61374..3ce4769 100644 --- a/ontouml_py/model/namedelement.py +++ b/ontouml_py/model/namedelement.py @@ -9,6 +9,7 @@ from typing import Any from typing import Optional +from icecream import ic from langstring import LangString from pydantic import Field from pydantic import field_validator @@ -61,7 +62,6 @@ def __ensure_non_empty(cls, checked_values: set[str], checked_field: ValidationI for elem in checked_values: if elem == "": error_message = format_error_message( - error_type="ValueError.", description=f"Invalid empty string in {cls.__name__} list.", cause=f"Empty string found in '{cls.__name__}' field {checked_field.field_name}.", solution=f"Ensure all elements in the {checked_field.field_name} list are non-empty strings.", @@ -81,6 +81,8 @@ def __init__(self, **data: dict[str, Any]) -> None: :param data: Fields to be set on the model instance, including inherited and class-specific attributes. :type data: dict[str, Any] """ + ic() + ic(data) # List of allowed subclasses: NamedElement is a categorizer of a complete generalization set self._validate_subclasses(["Diagram", "ModelElement", "Project"]) diff --git a/ontouml_py/model/ontoumlelement.py b/ontouml_py/model/ontoumlelement.py index d1c453f..ecf971f 100644 --- a/ontouml_py/model/ontoumlelement.py +++ b/ontouml_py/model/ontoumlelement.py @@ -13,6 +13,7 @@ from typing import Any from typing import Optional +from icecream import ic from pydantic import BaseModel from pydantic import Field @@ -59,10 +60,12 @@ def __init__(self, **data: dict[str, Any]) -> None: :type data: dict[str, Any] :raises ValueError: If 'modified' is set to a datetime earlier than 'created'. """ - self._validate_subclasses(["NamedElement", "Project", "ProjectElement", "Shape", "View"]) - + ic() + ic(data) # Sets attributes super().__init__(**data) + self._validate_subclasses(["NamedElement", "Project", "ProjectElement", "Shape", "View"]) + def __eq__(self, other: object) -> bool: """ @@ -113,7 +116,6 @@ def _validate_subclasses(cls, allowed_subclasses: list[str]) -> None: else: allowed = ", ".join(allowed_subclasses) error_message = format_error_message( - error_type="ValueError.", description=f"Invalid subclass type for class '{cls.__name__}'.", cause=f"'{cls.__name__}' is not an allowed subclass.", solution=f"Use one of the allowed subclasses: {allowed}.", diff --git a/ontouml_py/model/package.py b/ontouml_py/model/package.py index 12b4a1b..3b25d4a 100644 --- a/ontouml_py/model/package.py +++ b/ontouml_py/model/package.py @@ -8,133 +8,192 @@ """ from typing import Any +from icecream import ic from pydantic import PrivateAttr +from ontouml_py.model.anchor import Anchor +from ontouml_py.model.binaryrelation import BinaryRelation +from ontouml_py.model.class_py import Class +from ontouml_py.model.generalization import Generalization +from ontouml_py.model.generalizationset import GeneralizationSet +from ontouml_py.model.naryrelation import NaryRelation +from ontouml_py.model.note import Note from ontouml_py.model.packageable import Packageable from ontouml_py.utils.error_message import format_error_message class Package(Packageable): - """Represents a package in an OntoUML model, extending Packageable. - - A Package is a container for other Packageable contents, providing a way to group and organize these contents - within the OntoUML model. It supports operations to add and remove contents, ensuring the integrity and consistency - of the package's contents. - - :ivar _contents: A private set of Packageable contents contained within the package. - :vartype _contents: set[Packageable] - :cvar model_config: Configuration settings for the Pydantic model. - :vartype model_config: Dict[str, Any] - """ # Private attribute - _contents: set[Packageable] = PrivateAttr(default_factory=set) + _contents: dict[str, set[Packageable]] = PrivateAttr( + default={ + "Anchor": set(), + "BinaryRelation": set(), + "Class": set(), + "Generalization": set(), + "GeneralizationSet": set(), + "NaryRelation": set(), + "Note": set(), + "Package": set(), + } + ) model_config = { "arbitrary_types_allowed": True, - "validate_assignment": True, "extra": "forbid", "str_strip_whitespace": True, + "validate_assignment": True, "validate_default": True, } - def __init__(self, **data: dict[str, Any]) -> None: - """Initialize a new Package instance with specified attributes. - - This constructor sets up the package with the provided data, ensuring that all package-specific attributes - are correctly initialized. It also validates the 'contents' attribute to ensure it is a set, reflecting the - package's structure. - - :param data: Fields to be set on the model instance, including package-specific attributes. - :type data: dict[str, Any] - :raises TypeError: If 'contents' is provided and is not a set, ensuring correct data structure. - """ - super().__init__(**data) - - contents = data.get("contains") - if contents is not None and not isinstance(contents, set): - error_message = format_error_message( - error_type="Type Error", - description=f"Invalid type for 'contents' in Package with ID {self.id}.", - cause=f"Expected 'contents' to be a set, got {type(contents).__name__}.", - solution="Ensure 'contents' is provided as a set.", - ) - raise TypeError(error_message) - self._contents: set[Packageable] = contents if contents is not None else set() - - def add_content(self, content: Packageable) -> None: - """Add a new content to the package's collection of contents. - - This method ensures that only instances of Packageable or its subclasses are added to the package. It also - establishes a bidirectional relationship between the package and the content. - - :param content: The Packageable content to be added. - :type content: Packageable - :raises TypeError: If the provided content is not an instance of Packageable or if a package attempts to - contain itself. - """ - if content == self: - error_message = format_error_message( - error_type="Type Error", - description="Package cannot contain itself.", - cause=f"Attempted to add the package with ID {self.id} as its own content.", - solution="Ensure the content is not the package itself.", - ) - raise TypeError(error_message) - - if not isinstance(content, Packageable): - error_message = format_error_message( - error_type="Type Error", - description=f"Invalid content type in Package with ID {self.id}.", - cause=f"Expected Packageable instance, got {type(content).__name__} instance.", - solution="Ensure the content is an instance of Packageable.", - ) - raise TypeError(error_message) - - self._contents.add(content) # direct relation - content._Packageable__set_in_package(self) # inverse relation - - def remove_content(self, content: Packageable) -> None: - """Remove an existing content from the package's collection of contents. - - This method ensures that the content to be removed is actually part of the package. It also updates the - content's 'in_package' attribute to None, effectively breaking the bidirectional relationship. - - :param content: The Packageable content to be removed. - :type content: Packageable - :raises TypeError: If the content is not a valid Packageable. - :raises ValueError: If the content is not part of the package. - """ - if not isinstance(content, Packageable): - error_message = format_error_message( - error_type="Type Error", - description=f"Invalid content type for removal in Package with ID {self.id}.", - cause=f"Expected Packageable instance, got {type(content).__name__} instance.", - solution="Ensure the content is an instance of Packageable.", - ) - raise TypeError(error_message) - - if content not in self._contents: - error_message = format_error_message( - error_type="ValueError.", - description=f"Content not found in Package with ID {self.id}.", - cause=f"Content '{content}' is not part of the package's contents. Its contents are: {self._contents}.", - solution="Ensure that the content exists in the package before attempting to remove it.", - ) - raise ValueError(error_message) - - self._contents.remove(content) - content._Packageable__set_in_package(None) - - @property - def contents(self) -> set[Packageable]: - """Provide a read-only representation of the package's contents. - - This property is a safeguard to prevent direct modification of the 'contents' set. To add or remove contents, - use the 'add_content' and 'remove_content' methods. This design ensures that the integrity of the package's - contents collection is maintained. - - :return: A set of Packageable objects that are part of the package. - :rtype: set[Packageable] - """ + def __init__(self, project, **data: dict[str, Any]) -> None: + ic() + ic(project, data) + super().__init__(project, **data) + project._elements["Package"].add(self) + + def get_contents(self) -> dict: return self._contents + + def get_anchors(self) -> set[str]: + return self._contents["Anchor"] + + def get_binary_relations(self) -> set[str]: + return self._contents["BinaryRelation"] + + def get_classes(self) -> set[str]: + return self._contents["Class"] + + def get_generalizations(self) -> set[str]: + return self._contents["Generalization"] + + def get_generalization_sets(self) -> set[str]: + return self._contents["GeneralizationSet"] + + def get_nary_relations(self) -> set[str]: + return self._contents["NaryRelation"] + + def get_notes(self) -> set[str]: + return self._contents["Note"] + + def get_packages(self) -> set[str]: + return self._contents["Package"] + + + def get_content_by_id(self, content_type: str, content_id: str): + for internal_content in self._contents[content_type]: + if internal_content.id == content_id: + return internal_content + + def get_anchor_by_id(self, content_id: str): + return self.get_content_by_id("Anchor", content_id) + + def get_binary_relation_by_id(self, content_id: str): + return self.get_content_by_id("BinaryRelation", content_id) + + def get_class_by_id(self, content_id: str): + return self.get_content_by_id("Class", content_id) + + def get_generalization_by_id(self, content_id: str): + return self.get_content_by_id("Generalization", content_id) + + def get_generalization_set_by_id(self, content_id: str): + return self.get_content_by_id("GeneralizationSet", content_id) + + def get_nary_relation_by_id(self, content_id: str): + return self.get_content_by_id("NaryRelation", content_id) + + def get_note_by_id(self, content_id: str): + return self.get_content_by_id("Note", content_id) + + def get_package_by_id(self, content_id: str): + return self.get_content_by_id("Package", content_id) + + def add_anchor(self,new_content): + new_content.__Packageable__set_package(self) + self._contents["Anchor"].add(new_content) + + def add_binary_relation(self, new_content): + new_content.__Packageable__set_package(self) + self._contents["BinaryRelation"].add(new_content) + + def add_class(self,new_content): + new_content.__Packageable__set_package(self) + self._contents["Class"].add(new_content) + + def add_generalization(self,new_content): + new_content.__Packageable__set_package(self) + self._contents["Generalization"].add(new_content) + + def add_generalization_set(self,new_content): + new_content.__Packageable__set_package(self) + self._contents["GeneralizationSet"].add(new_content) + + def add_nary_relation(self,new_content): + new_content.__Packageable__set_package(self) + self._contents["NaryRelation"].add(new_content) + + def add_note(self,new_content): + new_content.__Packageable__set_package(self) + self._contents["Note"].add(new_content) + + def add_package(self,new_content): + new_content.__Packageable__set_package(self) + self._contents["Package"].add(new_content) + + def remove_anchor(self, old_content: Anchor) -> None: + if old_content not in self._contents["Anchor"]: + raise ValueError(self.__removal_error_message(old_content, "Anchor")) + self._contents["Anchor"].remove(old_content) + old_content.__Packageable__set_package(None) + + def remove_binary_relation(self, old_content: BinaryRelation) -> None: + if old_content not in self._contents["BinaryRelation"]: + raise ValueError(self.__removal_error_message(old_content, "BinaryRelation")) + self._contents["BinaryRelation"].remove(old_content) + old_content.__Packageable__set_package(None) + + def remove_class(self, old_content: Class) -> None: + if old_content not in self._contents["Class"]: + raise ValueError(self.__removal_error_message(old_content, "Class")) + self._contents["Class"].remove(old_content) + old_content.__Packageable__set_package(None) + + def remove_generalization(self, old_content: Generalization) -> None: + if old_content not in self._contents["Generalization"]: + raise ValueError(self.__removal_error_message(old_content, "Generalization")) + self._contents["Generalization"].remove(old_content) + old_content.__Packageable__set_package(None) + + def remove_generalization_set(self, old_content: GeneralizationSet) -> None: + if old_content not in self._contents["GeneralizationSet"]: + raise ValueError(self.__removal_error_message(old_content, "GeneralizationSet")) + self._contents["GeneralizationSet"].remove(old_content) + old_content.__Packageable__set_package(None) + + def remove_nary_relation(self, old_content: NaryRelation) -> None: + if old_content not in self._contents["NaryRelation"]: + raise ValueError(self.__removal_error_message(old_content, "NaryRelation")) + self._contents["NaryRelation"].remove(old_content) + old_content.__Packageable__set_package(None) + + def remove_note(self, old_content: Note) -> None: + if old_content not in self._contents["Note"]: + raise ValueError(self.__removal_error_message(old_content, "Note")) + self._contents["Note"].remove(old_content) + old_content.__Packageable__set_package(None) + + def remove_package(self, old_content) -> None: + if old_content not in self._contents["Package"]: + raise ValueError(self.__removal_error_message(old_content, "Package")) + self._contents["Package"].remove(old_content) + old_content.__Packageable__set_package(None) + + def __removal_error_message(self, old_content: Packageable, old_content_type: str) -> str: + error_message = format_error_message( + description=f"Invalid {old_content_type} content for removal.", + cause=f"The content {old_content} is not found in the {old_content_type} contents of the package with ID {self.id}.", + solution=f"Ensure the content to be removed is a valid {old_content_type} content in the package.", + ) + return error_message + diff --git a/ontouml_py/model/packageable.py b/ontouml_py/model/packageable.py index ac5841c..8745b6e 100644 --- a/ontouml_py/model/packageable.py +++ b/ontouml_py/model/packageable.py @@ -10,6 +10,7 @@ from typing import Any from typing import Optional +from icecream import ic from pydantic import PrivateAttr from ontouml_py.model.modelelement import ModelElement @@ -28,42 +29,35 @@ class Packageable(ModelElement): :vartype model_config: Dict[str, Any] """ - _in_package: Optional["Package"] = PrivateAttr(default=None) # noqa: F821 (flake8) + _package: Optional["Package"] = PrivateAttr(default=None) model_config = { - "validate_assignment": True, + "arbitrary_types_allowed": True, "extra": "forbid", "str_strip_whitespace": True, + "validate_assignment": True, "validate_default": True, } @abstractmethod - def __init__(self, **data: dict[str, Any]) -> None: + def __init__(self,project, **data: dict[str, Any]) -> None: """Initialize a new Packageable instance. This method is abstract and should be implemented by subclasses. Ensures that the element is part of a valid subclass and sets initial attributes. It also enforces that the - 'in_package' attribute is not directly initialized, as it is a private property managed by the Package class. + '_package' attribute is not directly initialized, as it is a private property managed by the Package class. - :param data: Fields to be set on the model instance, excluding 'in_package'. + :param data: Fields to be set on the model instance, excluding '_package'. :type data: dict[str, Any] - :raises ValueError: If 'in_package' is directly initialized. + :raises ValueError: If '_package' is directly initialized. """ + ic() + ic(project, data) + super().__init__(project,**data) self._validate_subclasses(["Package", "Generalization", "GeneralizationSet", "Classifier", "Note", "Anchor"]) - super().__init__(**data) @property - def in_package(self) -> Optional["Package"]: # noqa: F821 (flake8) - """Read-only property to access the package this element is contained in. - - :return: The Package instance containing this element, if any. - :rtype: Optional[Package] - """ - return self._in_package - - def __set_in_package(self, new_package: "Package") -> None: # noqa: F821 (flake8) - """Set the package this element is contained in. This method is private and should be used internally. + def package(self) -> Optional["Package"]: # noqa: F821 (flake8) + return self._package - :param new_package: The Package instance to set as the container of this element. - :type new_package: Package - """ - self._in_package = new_package + def __set_package(self, owner_package: Optional["Package"]) -> None: # noqa: F821 (flake8) + self._package = owner_package diff --git a/ontouml_py/model/project.py b/ontouml_py/model/project.py index fdc3efa..a3e01ac 100644 --- a/ontouml_py/model/project.py +++ b/ontouml_py/model/project.py @@ -1,6 +1,7 @@ from typing import Any from typing import Optional +from icecream import ic from pydantic import Field from pydantic import PrivateAttr @@ -60,13 +61,15 @@ class Project(NamedElement): model_config = { "arbitrary_types_allowed": True, - "validate_assignment": True, "extra": "forbid", "str_strip_whitespace": True, + "validate_assignment": True, "validate_default": True, } def __init__(self, **data: dict[str, Any]) -> None: + ic() + ic(data) super().__init__(**data) def get_elements(self) -> dict: @@ -111,17 +114,62 @@ def get_shapes(self) -> set[str]: def get_views(self) -> set[str]: return self._elements["View"] - def create_anchor(self): - new_element = Anchor(self) + def get_element_by_id(self, element_type: str, element_id: str): + for internal_element in self._elements[element_type]: + if internal_element.id == element_id: + return internal_element + + def get_anchor_by_id(self, element_id: str): + return self.get_element_by_id("Anchor", element_id) + + def get_binary_relation_by_id(self, element_id: str): + return self.get_element_by_id("BinaryRelation", element_id) + + def get_class_by_id(self, element_id: str): + return self.get_element_by_id("Class", element_id) + + def get_diagram_by_id(self, element_id: str): + return self.get_element_by_id("Diagram", element_id) + + def get_generalization_by_id(self, element_id: str): + return self.get_element_by_id("Generalization", element_id) + + def get_generalization_set_by_id(self, element_id: str): + return self.get_element_by_id("GeneralizationSet", element_id) + + def get_literal_by_id(self, element_id: str): + return self.get_element_by_id("Literal", element_id) + + def get_nary_relation_by_id(self, element_id: str): + return self.get_element_by_id("NaryRelation", element_id) + + def get_note_by_id(self, element_id: str): + return self.get_element_by_id("Note", element_id) + + def get_package_by_id(self, element_id: str): + return self.get_element_by_id("Package", element_id) + + def get_property_by_id(self, element_id: str): + return self.get_element_by_id("Property", element_id) + + def get_shape_by_id(self, element_id: str): + return self.get_element_by_id("Shape", element_id) + + def get_view_by_id(self, element_id: str): + return self.get_element_by_id("View", element_id) + + + def create_anchor(self, **data: dict[str, Any]): + new_element = Anchor(self, **data) self._elements["Anchor"].add(new_element) - def create_binary_relation(self): - new_element = BinaryRelation(self) + def create_binary_relation(self, **data: dict[str, Any]): + new_element = BinaryRelation(self, **data) self._elements["BinaryRelation"].add(new_element) return new_element - def create_class(self): - new_element = Class(self) + def create_class(self, **data: dict[str, Any]): + new_element = Class(self, **data) self._elements["Class"].add(new_element) return new_element @@ -130,28 +178,28 @@ def create_diagram(self, **data: dict[str, Any]): self._elements["Diagram"].add(new_element) return new_element - def create_generalization(self): - new_element = Generalization(self) + def create_generalization(self, **data: dict[str, Any]): + new_element = Generalization(self, **data) self._elements["Generalization"].add(new_element) return new_element - def create_generalization_set(self): - new_element = GeneralizationSet(self) + def create_generalization_set(self, **data: dict[str, Any]): + new_element = GeneralizationSet(self, **data) self._elements["GeneralizationSet"].add(new_element) return new_element - def create_nary_relation(self): - new_element = NaryRelation(self) + def create_nary_relation(self, **data: dict[str, Any]): + new_element = NaryRelation(self, **data) self._elements["NaryRelation"].add(new_element) return new_element - def create_note(self): - new_element = Note(self) + def create_note(self, **data: dict[str, Any]): + new_element = Note(self, **data) self._elements["Note"].add(new_element) return new_element - def create_package(self): - new_element = Package(self) + def create_package(self, **data: dict[str, Any]): + new_element = Package(self, **data) self._elements["Package"].add(new_element) return new_element @@ -208,46 +256,3 @@ def __deletion_error_message(self, old_element: ProjectElement, old_element_type ) return error_message - def get_element_by_id(self, element_type: str, element_id: str): - for internal_element in self._elements[element_type]: - if internal_element.id == element_id: - return internal_element - - def get_anchor_by_id(self, element_id: str): - return self.get_element_by_id("Anchor", element_id) - - def get_binary_relation_by_id(self, element_id: str): - return self.get_element_by_id("BinaryRelation", element_id) - - def get_class_by_id(self, element_id: str): - return self.get_element_by_id("Class", element_id) - - def get_diagram_by_id(self, element_id: str): - return self.get_element_by_id("Diagram", element_id) - - def get_generalization_by_id(self, element_id: str): - return self.get_element_by_id("Generalization", element_id) - - def get_generalization_set_by_id(self, element_id: str): - return self.get_element_by_id("GeneralizationSet", element_id) - - def get_literal_by_id(self, element_id: str): - return self.get_element_by_id("Literal", element_id) - - def get_nary_relation_by_id(self, element_id: str): - return self.get_element_by_id("NaryRelation", element_id) - - def get_note_by_id(self, element_id: str): - return self.get_element_by_id("Note", element_id) - - def get_package_by_id(self, element_id: str): - return self.get_element_by_id("Package", element_id) - - def get_property_by_id(self, element_id: str): - return self.get_element_by_id("Property", element_id) - - def get_shape_by_id(self, element_id: str): - return self.get_element_by_id("Shape", element_id) - - def get_view_by_id(self, element_id: str): - return self.get_element_by_id("View", element_id) diff --git a/ontouml_py/model/projectelement.py b/ontouml_py/model/projectelement.py index 48d2fa5..07ca458 100644 --- a/ontouml_py/model/projectelement.py +++ b/ontouml_py/model/projectelement.py @@ -8,6 +8,7 @@ from abc import abstractmethod from typing import Any +from icecream import ic from pydantic import PrivateAttr from ontouml_py import model @@ -32,9 +33,9 @@ class ProjectElement(OntoumlElement): model_config = { "arbitrary_types_allowed": True, - "validate_assignment": True, "extra": "forbid", "str_strip_whitespace": True, + "validate_assignment": True, "validate_default": True, } @@ -50,6 +51,8 @@ def __init__(self, project: "Project", **data: dict[str, Any]) -> None: :raises ValueError: If 'modified' is set to a datetime earlier than 'created', or if 'in_project' is directly initialized. """ + ic() + ic(project, data) if not isinstance(project, model.project.Project): raise TypeError("unallowed type") diff --git a/tests/concrete_classes/test_package_packageable.py b/tests/concrete_classes/test_package_packageable.py index ad2ecde..81b8620 100644 --- a/tests/concrete_classes/test_package_packageable.py +++ b/tests/concrete_classes/test_package_packageable.py @@ -99,7 +99,7 @@ def test_packageable_in_package_property(packageable_content: Packageable, empty :param empty_package: Fixture providing an empty Package instance. """ empty_package.add_content(packageable_content) - assert packageable_content.in_package == empty_package, "'in_package' should reference the containing package." + assert packageable_content.package == empty_package, "'in_package' should reference the containing package." def test_packageable_in_package_setter_restriction(packageable_content: Packageable) -> None: