From 6d18c70bcb39cd832e6442f0c4a5df445c4123a1 Mon Sep 17 00:00:00 2001
From: "Hock, Martin" <martin.hock@fst.tu-darmstadt.de>
Date: Thu, 2 Mar 2023 18:52:51 +0100
Subject: [PATCH] Add docstrings, and reformat docstring to black and flake8
 with max-line-length of 88.

---
 functions/classes.py | 269 +++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 261 insertions(+), 8 deletions(-)

diff --git a/functions/classes.py b/functions/classes.py
index 758fbb1..7563deb 100644
--- a/functions/classes.py
+++ b/functions/classes.py
@@ -1,5 +1,6 @@
 """
-File consists of several classes for the different elements of a device.
+File consists of several classes to model elements and assembly
+layers of a composed device.
 """
 from __future__ import annotations
 import uuid
@@ -10,6 +11,10 @@ from typing import List, Dict, Optional
 
 
 class AggregationLayer(Enum):
+    """Describes the levl of aggregation for the objects LegoComponent
+    and LegoAssembly and provides the 4 applicable layers.
+    """
+
     SYSTEM = auto()
     ASSEMBLY = auto()
     SUBASSEMBLY = auto()
@@ -17,6 +22,34 @@ class AggregationLayer(Enum):
 
 
 class LegoComponent:
+    """
+    A class for storing information about a single Lego component.
+
+    ...
+
+    Attributes
+    ----------
+    uuid : UUID
+        A randomly generated unique identifier for the component.
+    parent : None | LegoAssembly
+        The parent of the component. Can be either None or a LegoAssembly object.
+    layer : AggregationLayer
+        An enumeration indicating the hierarchy level. For compoennts, this is
+        COMPONENT by default.
+    properties : dict
+        Dictionary that holds all properties of the component that can be saved
+        in a dictionary format.
+
+    Methods
+    -------
+    clone(new_label=None)
+        Returns a new instance of LegoComponent identical to the current instance.
+    get_root_assembly()
+        Returns the top-level assembly in which the component belongs.
+    to_dict()
+        Returns the current instance represented as a dictionary.
+    """
+
     def __init__(
         self,
         label: Optional[str] = None,
@@ -24,6 +57,25 @@ class LegoComponent:
         *more_properties: dict,
         **kwargs,
     ) -> None:
+        """
+        Constructs all the necessary attributes for the LegoComponent object.
+
+        Parameters
+        ----------
+            label : str, optional
+                The name of the component to add.
+            datasheet : dict, optional
+                Metadata describing the component, read from datasheet.
+            more_properties : tuple of dict, dict
+                Additional dictionaries representing custom properties.
+            kwargs
+                Arbitrary keyword arguments representing custom properties.
+
+        Raises
+        ------
+            ValueError
+                If the type of more_properties argument is not a dictionary.
+        """
         self.uuid: uuid.UUID = uuid.uuid4()
         self.parent: None | LegoAssembly = None
         self.layer: AggregationLayer = AggregationLayer.COMPONENT
@@ -41,6 +93,24 @@ class LegoComponent:
             self.properties[key] = value
 
     def clone(self, new_label: Optional[str] = None) -> LegoComponent:
+        """
+        Returns a new instance of LegoComponent identical to the current instance.
+
+        This method creates a new instance of LegoComponent that is identical to the
+        current instance with an optional different label.
+        The assigned uuid changes and elements are copied.
+
+        Parameters
+        ----------
+            new_label : str, optional
+                A new label string to use. Defaults to None.
+
+        Returns
+        -------
+        LegoComponent
+            A new instance of LegoComponent that is identical to the current instance.
+
+        """
         if new_label is None:
             new_label = self.properties["label"]
         clone = LegoComponent(None, None, self.properties)
@@ -48,6 +118,18 @@ class LegoComponent:
         return clone
 
     def get_root_assembly(self):
+        """
+        Returns the top-level assembly in which the component belongs.
+
+        This method traverses the parent hierarchy of a LegoComponent object until it
+        finds the root-level LegoAssembly, returning it to the caller. A parent is
+        assigned when a LegoComponent or LegoItem is added to a LegoAssembly object.
+
+        Returns
+        -------
+        None | LegoAssembly
+            The root-level LegoAssembly or None if the component has no parent.
+        """
         if self.parent is None:
             return None
         current_assembly = self.parent
@@ -56,6 +138,17 @@ class LegoComponent:
         return current_assembly
 
     def to_dict(self) -> Dict:
+        """
+        Returns the current instance represented as a dictionary.
+
+        This method returns a dictionary representation of the LegoComponent object
+        suitable for serialization as JSON.
+
+        Returns
+        -------
+        dict
+            A dictionary representation of the object.
+        """
         dict_ = {
             "uuid": self.uuid,
             "properties": self.properties,
@@ -64,13 +157,31 @@ class LegoComponent:
         return {"component": dict_}
 
     def __str__(self):
+        """
+        Simple string representation of the object.
+        """
         return self.__repr__()
 
     def __repr__(self):
+        """
+        String representation of the object including the component label and UUID.
+        """
         return f"LegoComponent {self.properties['label']} [{self.uuid}]"
 
 
 class LegoAssembly:
+    """
+    Represents a Lego assembly that can contain Lego Components and subassemblies.
+
+    Attributes:
+        uuid (uuid.UUID): The unique ID of the assembly.
+        parent (LegoAssembly or None): The parent assembly containing this one, if any.
+        properties (dict): Optional properties for the assembly, such as a label.
+        layer (AggregationLayer): The aggregation layer of the assembly.
+        components (List[LegoComponent]): The list of contained components.
+        assemblies (List[LegoAssembly]): The list of contained subassemblies.
+    """
+
     def __init__(
         self,
         layer: AggregationLayer,
@@ -78,6 +189,16 @@ class LegoAssembly:
         *properties: dict,
         **kwargs,
     ) -> None:
+        """
+        Initializes a new LegoAssembly instance.
+
+        Args:
+            layer (AggregationLayer): The aggregation layer of the assembly.
+            label (Optional[str], optional): Optional label for the assembly.
+                Defaults to None.
+            properties (dict): Optional properties for the assembly, such as a label.
+            **kwargs: Optional keyword arguments for additional properties.
+        """
         self.uuid: uuid.UUID = uuid.uuid4()
         self.parent: None | LegoAssembly = None
         self.properties: dict = {}
@@ -95,6 +216,20 @@ class LegoAssembly:
         self.assemblies: List[LegoAssembly] = []
 
     def add_component(self, component: LegoComponent | List[LegoComponent]) -> None:
+        """
+        Adds a Lego component to the current assembly.
+        Use add() to handle both components and assemblies.
+
+        Args:
+            component (LegoComponent or List[LegoComponent]):
+                The component or list of components to add.
+
+        Raises:
+            TypeError: If the argument is not a LegoComponent instance
+                or a list of LegoComponent instances.
+            AssertionError: If the component or a component with the same UUID
+            is already contained in the assembly or any of its child assemblies.
+        """
         if isinstance(component, list):
             for c in component:
                 self.add_component(c)
@@ -116,6 +251,20 @@ class LegoAssembly:
         self.components.append(component)
 
     def add_assembly(self, assembly: LegoAssembly | List[LegoAssembly]) -> None:
+        """
+        Adds a subassembly to the current assembly.
+        Use add() to handle both components and assemblies.
+
+        Args:
+            assembly (LegoAssembly or List[LegoAssembly]):
+                The subassembly or list of subassemblies to add.
+
+        Raises:
+            TypeError: If the argument is not a LegoAssembly instance
+                or a list of LegoAssembly instances.
+            AssertionError: If the subassembly or a subassembly with the same UUID
+                is already contained in the assembly or any of its child assemblies.
+        """
         if isinstance(assembly, list):
             for a in assembly:
                 self.add_assembly(a)
@@ -138,6 +287,18 @@ class LegoAssembly:
     def add(
         self, part: LegoAssembly | LegoComponent | List[LegoAssembly | LegoComponent]
     ) -> None:
+        """
+        Adds either a Lego component, a subassembly or a (mixed) list of them to the
+        current assembly. Uses internal functions add_component() and add_assembly().
+
+        Args:
+            part (LegoAssembly or LegoComponent or List[LegoAssembly or LegoComponent]):
+                The part or parts to add.
+
+        Raises:
+            TypeError: If the argument is not a LegoAssembly instance or a LegoComponent
+                instance, or a (mixed) list of them.
+        """
         if isinstance(part, LegoComponent):
             self.add_component(part)
         elif isinstance(part, LegoAssembly):
@@ -153,9 +314,29 @@ class LegoAssembly:
             )
 
     def children(self) -> Dict[str, List[LegoComponent] | List[LegoAssembly]]:
+        """
+        Returns a dictionary of the assembly's children (components and
+        sub-assemblies), sorted by category.
+
+        Returns:
+            A dictionary with two keys - "components" and "assemblies" - and
+            corresponding lists of LegoComponents and LegoAssemblies as values.
+        """
         return {"components": self.components, "assemblies": self.assemblies}
 
     def get_component_list(self, max_depth: int = -1) -> List[LegoComponent]:
+        """
+        Gets a full list of all components contained in the assembly or any of
+        its sub-assemblies.
+
+        Args:
+            max_depth: An integer indicating the maximum depth of recursion.
+                -1 means unlimited depth. 0 returns only direc children.
+
+        Returns:
+            A list of all LegoComponents contained in the current assembly or
+            its sub-assemblies.
+        """
         component_list = []
         component_list.extend(self.components)
         if max_depth > 0 or max_depth < 0:
@@ -164,41 +345,78 @@ class LegoAssembly:
         return component_list
 
     def get_root_assembly(self) -> LegoAssembly:
+        """
+        Returns the root LegoAssembly of the current assembly by recursively
+        searching up the tree until the top-most parent (the root) is found.
+
+        Returns:
+            The root LegoAssembly of the current LegoAssembly instance.
+        """
         current_assembly = self
         while current_assembly.parent is not None:
             current_assembly = current_assembly.parent
         return current_assembly
 
     def contains_uuid(self, uuid_: uuid.UUID):
-        # check component ids
+        """
+        Recursively searches through the assembly  and component
+        tree to determine whether the specified UUID exists anywhere
+        within it.
+
+        Args:
+            uuid_: A UUID object representing the ID to search for.
+
+        Returns:
+            True if the UUID exists in the assembly or any of its sub-assemblies,
+                False otherwise.
+        """
         component_ids = list(map(lambda c: c.uuid, self.components))
         if uuid_ in component_ids:
             return True
-        # check assembly ids
         assembly_ids = list(map(lambda a: a.uuid, self.assemblies))
         if uuid_ in assembly_ids:
             return True
-        # recursively check assemblies
         for assembly in self.assemblies:
             if assembly.contains_uuid(uuid_):
                 return True
         return False
 
     def to_dict(self) -> Dict:
+        """
+        Serializes the current LegoAssembly instance and its descendants into a dict.
+
+        Returns:
+            A dictionary representation of the current assembly, including all
+            of its component and sub-assembly children.
+        """
         dict_ = {
             "uuid": self.uuid,
             "properties": self.properties,
             "layer": self.layer,
         }
-        # store components
         dict_["components"] = [component.to_dict() for component in self.components]
         dict_["assemblies"] = [assembly.to_dict() for assembly in self.assemblies]
         return {"assembly": dict_}
 
     def __repr__(self):
+        """
+        String representation of the object including the component label and UUID.
+        """
         return f"LegoAssembly {self.properties['label']} [{self.uuid}]"
 
     def clone(self, label: Optional[str] = None) -> LegoAssembly:
+        """
+        Creates a deep clone of the current LegoAssembly instance, including
+        all of its component and sub-assembly children. The assigned uuid changes.
+        Optionally a new label can be passed.
+
+        Args:
+            label: The label (name) for the cloned assembly. If none is passed,
+            uses the same label as the original assembly.
+
+        Returns:
+            The cloned LegoAssembly instance.
+        """
         if label is None:
             label = self.properties["label"]
         clone = LegoAssembly(self.layer, None, self.properties)
@@ -211,12 +429,22 @@ class LegoAssembly:
 
 
 def print_assembly_tree(root, level=0, is_last=False):
+    """
+    Prints the assembly tree starting from root with a visualization
+    implemented with text characters.
+
+    Args:
+        root (LegoAssembly): The root of the assembly tree to print.
+        level (int): The indentation level. Defaults to 0.
+        is_last (bool): Determines whether the current node is the last in level.
+            Defaults to False.
+    """
     if not isinstance(root, LegoAssembly):
         raise TypeError(
             f"Argument should be of type {LegoAssembly.__name__}, "
             f"got {type(root).__name__} instead."
         )
-    # print component
+    """ Print the items. """
     assembly_padding = ""
     if level > 0:
         assembly_padding += "│   " * (level - 1)
@@ -225,11 +453,11 @@ def print_assembly_tree(root, level=0, is_last=False):
         else:
             assembly_padding += "├── "
     print(f"{assembly_padding}{root}")
-    # recursively print child components
+    """ Recursively print child components. """
     for i, assembly in enumerate(root.assemblies):
         is_last_ = i == len(root.assemblies) - 1 and len(root.components) == 0
         print_assembly_tree(assembly, level + 1, is_last_)
-    # print items
+    """ Print the components. """
     for i, item in enumerate(root.components):
         component_padding = "│   " * level if not is_last else "    "
         component_padding += "├── " if i < len(root.components) - 1 else "└── "
@@ -237,6 +465,18 @@ def print_assembly_tree(root, level=0, is_last=False):
 
 
 def correct_aggregation_hierarchy(root: LegoAssembly, strict: bool = False):
+    """
+    Recursively checks whether the aggregation hierarchy from `root` is correct.
+
+    Args:
+        root (LegoAssembly): The root of the assembly tree.
+        strict (bool): If True, the function will return False if any assembly
+            or component with a layer level equal to root's is found.
+            Defaults to False.
+
+    Returns:
+        True if the aggregation hierarchy is correct. False otherwise.
+    """
     if not isinstance(root, LegoAssembly):
         raise TypeError(
             f"Argument should be of type {LegoAssembly.__name__}, "
@@ -257,7 +497,20 @@ def correct_aggregation_hierarchy(root: LegoAssembly, strict: bool = False):
 
 
 class KPIEncoder(json.JSONEncoder):
+    """
+    JSON encoder that handles special class types for KPI serialization.
+    """
+
     def default(self, o):
+        """
+        Overrides default method to handle special conversion cases.
+
+        Args:
+            o : Object to be converted.
+
+        Returns:
+            Converted object or super method if no applicable case is found.
+        """
         if isinstance(o, uuid.UUID):
             return "kpi-" + str(o)
         if isinstance(o, (AggregationLayer)):
-- 
GitLab