Repository: Sage-Bionetworks/schematic (language: python)

Issue:
[bug fix] Updated workflow to install dependencies and root project together
## Context
When installing dependencies, we had: 
```
        if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
```
which made that github action skipped dependencies installation after `poetry install --only-root`. See screen shot that made the action failed. 

<img width="1063" alt="Screen Shot 2024-02-23 at 1 48 22 PM" src="https://github.com/Sage-Bionetworks/schematic/assets/55448354/4b6ed1f4-347d-4c6f-af1c-c076b93f5653">

I think it makes sense to just install root and dependencies together. 
I tested it locally to make sure that the project itself could be installed: 
```
(schematicpy-py3.10) lpeng@ip-192-168-0-2 schematic % poetry install --no-interaction --all-extras
Configuration file exists at /Users/lpeng/Library/Preferences/pypoetry, reusing this directory.

Consider moving TOML configuration files to /Users/lpeng/Library/Application Support/pypoetry, as support for the legacy directory will be removed in an upcoming release.
Installing dependencies from lock file

No dependencies to install or update

Installing the current project: schematicpy (23.11.1)

```

FDS-258 mypy visualization module
1. Fixes [FDS-258]
2. Visualization module fully typyed
3. Visualization module checked by github workflow

[FDS-258]: https://sagebionetworks.jira.com/browse/FDS-258?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/pdoc.yml b/.github/workflows/pdoc.yml
index 1a1627c99..187b5adb1 100644
--- a/.github/workflows/pdoc.yml
+++ b/.github/workflows/pdoc.yml
@@ -65,11 +65,11 @@ jobs:
           key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }}
 
       #----------------------------------------------
-      # install dependencies if cache does not exist 
+      # install dependencies and root project
       #----------------------------------------------
-      - name: Install dependencies
+      - name: Install dependencies and root project
         if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
-        run: poetry install --no-interaction --no-root
+        run: poetry install --no-interaction --all-extras
 
       # create documentation
       - run: poetry add pdoc@13.0.0
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 804ef19e2..eb2ebafcb 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -44,18 +44,12 @@ jobs:
           key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }}
 
       #----------------------------------------------
-      # install dependencies if cache does not exist 
+      # install dependencies and root project
       #----------------------------------------------
-      - name: Install dependencies
+      - name: Install dependencies and root project
         if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
-        run: poetry install --no-interaction --no-root
+        run: poetry install --no-interaction --all-extras
         
-      #----------------------------------------------
-      #    install your root project, if required 
-      #----------------------------------------------      
-      - name: Install library
-        run: poetry install --no-interaction
-
       #----------------------------------------------
       #    get current pushed tag
       #----------------------------------------------      
diff --git a/schematic/help.py b/schematic/help.py
index a7adcfc77..60f25a2c5 100644
--- a/schematic/help.py
+++ b/schematic/help.py
@@ -2,6 +2,33 @@
 # pylint: disable=line-too-long
 #!/usr/bin/env python3
 
+from typing import get_args
+from schematic.utils.schema_utils import DisplayLabelType
+from schematic.visualization.tangled_tree import FigureType, TextType
+
+
+DATA_MODEL_LABELS_DICT = {
+    "display_label": "use the display name as a label, if it is valid (contains no blacklisted characters) otherwise will default to class_label.",
+    "class_label": "default, use standard class or property label.",
+}
+# Ensure that all DisplayLabelTypes have a description
+assert sorted(DATA_MODEL_LABELS_DICT.keys()) == sorted(get_args(DisplayLabelType))
+
+# Combine each label and its description into one string
+DATA_MODEL_LABELS_LIST = [
+    f"{label}, {description}" for label, description in DATA_MODEL_LABELS_DICT.items()
+]
+
+DATA_MODEL_LABELS_HELP = (
+    "Choose how to set the label in the data model. "
+    f"{' '.join(DATA_MODEL_LABELS_LIST)} "
+    "Do not change from default unless there is a real need, using 'display_label' can have consequences if not used properly."
+)
+
+
+FIGURE_TYPES = " or ".join([f"'{item}'" for item in get_args(FigureType)])
+TEXT_TYPES = " or ".join([f"'{item}'" for item in get_args(TextType)])
+
 # `schematic manifest` related sub-commands description
 manifest_commands = {
     "manifest": {
@@ -54,12 +81,7 @@
                 "Specify to alphabetize valid attribute values either ascending (a) or descending (d)."
                 "Optional"
             ),
-            "data_model_labels": (
-                "Choose how to set the label in the data model. "
-                "display_label, use the display name as a label, if it is valid (contains no blacklisted characters) otherwise will default to class_label. "
-                "class_label, default, use standard class or property label. "
-                "Do not change from default unless there is a real need, using 'display_label' can have consequences if not used properly."
-            ),
+            "data_model_labels": DATA_MODEL_LABELS_HELP,
         },
         "migrate": {
             "short_help": (
@@ -139,12 +161,7 @@
                 "class_label, display_label, display_name, default, class_label. When true annotations and table columns will be uploaded with the display name formatting with blacklisted characters removed. "
                 "To use for tables, use in conjunction with the use_schema_label flag."
             ),
-            "data_model_labels": (
-                "Choose how to set the label in the data model. "
-                "display_label, use the display name as a label, if it is valid (contains no blacklisted characters) otherwise will default to class_label. "
-                "class_label, default, use standard class or property label. "
-                "Do not change from default unless there is a real need, using 'display_label' can have consequences if not used properly."
-            ),
+            "data_model_labels": DATA_MODEL_LABELS_HELP,
         },
         "validate": {
             "short_help": ("Validation of manifest files."),
@@ -170,12 +187,7 @@
             "project_scope": (
                 "Specify a comma-separated list of projects to search through for cross manifest validation."
             ),
-            "data_model_labels": (
-                "Choose how to set the label in the data model. "
-                "display_label, use the display name as a label, if it is valid (contains no blacklisted characters) otherwise will default to class_label. "
-                "class_label, default, use standard class or property label. "
-                "Do not change from default unless there is a real need, using 'display_label' can have consequences if not used properly."
-            ),
+            "data_model_labels": DATA_MODEL_LABELS_HELP,
         },
     }
 }
@@ -191,12 +203,7 @@
             "output_jsonld": (
                 "Path to where the generated JSON-LD file needs to be outputted."
             ),
-            "data_model_labels": (
-                "Choose how to set the label in the data model. "
-                "display_label, use the display name as a label, if it is valid (contains no blacklisted characters) otherwise will default to class_label. "
-                "class_label, default, use standard class or property label. "
-                "Do not change from default unless there is a real need, using 'display_label' can have consequences if not used properly."
-            ),
+            "data_model_labels": DATA_MODEL_LABELS_HELP,
         }
     }
 }
@@ -219,17 +226,12 @@
         ),
         "tangled_tree": {
             "figure_type": (
-                "Specify the type of schema visualization to make. Either 'dependency' or 'component'."
+                f"Specify the type of schema visualization to make. Either {FIGURE_TYPES}."
             ),
             "text_format": (
-                "Specify the type of text to gather for tangled tree visualization, either 'plain' or 'highlighted'."
-            ),
-            "data_model_labels": (
-                "Choose how to set the label in the data model. "
-                "display_label, use the display name as a label, if it is valid (contains no blacklisted characters) otherwise will default to class_label. "
-                "class_label, default, use standard class or property label. "
-                "Do not change from default unless there is a real need, using 'display_label' can have consequences if not used properly."
+                f"Specify the type of text to gather for tangled tree visualization, either {TEXT_TYPES}."
             ),
+            "data_model_labels": DATA_MODEL_LABELS_HELP,
         },
     }
 }
diff --git a/schematic/manifest/commands.py b/schematic/manifest/commands.py
index fcf0c1b4b..be648b2b4 100644
--- a/schematic/manifest/commands.py
+++ b/schematic/manifest/commands.py
@@ -2,7 +2,7 @@
 import logging
 from pathlib import Path
 import sys
-from typing import List
+from typing import get_args, List
 import click
 import click_log
 
@@ -10,6 +10,7 @@
 from schematic.schemas.data_model_graph import DataModelGraph, DataModelGraphExplorer
 from schematic.manifest.generator import ManifestGenerator
 
+from schematic.utils.schema_utils import DisplayLabelType
 from schematic.utils.cli_utils import log_value_from_config, query_dict, parse_syn_ids
 from schematic.utils.google_api_utils import export_manifest_csv
 from schematic.help import manifest_commands
@@ -109,7 +110,7 @@ def manifest(ctx, config):  # use as `schematic manifest ...`
     "--data_model_labels",
     "-dml",
     default="class_label",
-    type=click.Choice(["display_label", "class_label"], case_sensitive=True),
+    type=click.Choice(list(get_args(DisplayLabelType)), case_sensitive=True),
     help=query_dict(manifest_commands, ("manifest", "get", "data_model_labels")),
 )
 @click.pass_obj
diff --git a/schematic/models/commands.py b/schematic/models/commands.py
index f7af66456..81a4a3286 100644
--- a/schematic/models/commands.py
+++ b/schematic/models/commands.py
@@ -1,5 +1,6 @@
 #!/usr/bin/env python3
 
+from typing import get_args
 from gc import callbacks
 import logging
 import sys
@@ -20,6 +21,7 @@
 from schematic.help import model_commands
 from schematic.exceptions import MissingConfigValueError
 from schematic.configuration.configuration import CONFIG
+from schematic.utils.schema_utils import DisplayLabelType
 
 logger = logging.getLogger("schematic")
 click_log.basic_config(logger)
@@ -112,7 +114,7 @@ def model(ctx, config):  # use as `schematic model ...`
     "--data_model_labels",
     "-dml",
     default="class_label",
-    type=click.Choice(["display_label", "class_label"], case_sensitive=True),
+    type=click.Choice(list(get_args(DisplayLabelType)), case_sensitive=True),
     help=query_dict(model_commands, ("model", "submit", "data_model_labels")),
 )
 @click.option(
diff --git a/schematic/schemas/commands.py b/schematic/schemas/commands.py
index f9660764c..9836c792f 100644
--- a/schematic/schemas/commands.py
+++ b/schematic/schemas/commands.py
@@ -6,12 +6,14 @@
 import sys
 import time
 import re
+from typing import get_args
 
 from schematic.schemas.data_model_parser import DataModelParser
 from schematic.schemas.data_model_graph import DataModelGraph, DataModelGraphExplorer
 from schematic.schemas.data_model_validator import DataModelValidator
 from schematic.schemas.data_model_jsonld import DataModelJsonLD, convert_graph_to_jsonld
 
+from schematic.utils.schema_utils import DisplayLabelType
 from schematic.utils.cli_utils import query_dict
 from schematic.utils.schema_utils import export_schema
 from schematic.help import schema_commands
@@ -45,7 +47,7 @@ def schema():  # use as `schematic model ...`
     "--data_model_labels",
     "-dml",
     default="class_label",
-    type=click.Choice(["display_label", "class_label"], case_sensitive=True),
+    type=click.Choice(list(get_args(DisplayLabelType)), case_sensitive=True),
     help=query_dict(schema_commands, ("schema", "convert", "data_model_labels")),
 )
 @click.option(
diff --git a/schematic/visualization/attributes_explorer.py b/schematic/visualization/attributes_explorer.py
index 031fe5756..420dd4f0f 100644
--- a/schematic/visualization/attributes_explorer.py
+++ b/schematic/visualization/attributes_explorer.py
@@ -2,6 +2,7 @@
 import json
 import logging
 import os
+from typing import Optional, no_type_check
 
 import numpy as np
 import pandas as pd
@@ -9,6 +10,7 @@
 from schematic.schemas.data_model_parser import DataModelParser
 from schematic.schemas.data_model_graph import DataModelGraph, DataModelGraphExplorer
 from schematic.schemas.data_model_json_schema import DataModelJSONSchema
+from schematic.utils.schema_utils import DisplayLabelType
 from schematic.utils.io_utils import load_json
 
 logger = logging.getLogger(__name__)
@@ -20,7 +22,7 @@ class AttributesExplorer:
     def __init__(
         self,
         path_to_jsonld: str,
-        data_model_labels: str,
+        data_model_labels: DisplayLabelType,
     ) -> None:
         self.path_to_jsonld = path_to_jsonld
 
@@ -66,7 +68,7 @@ def create_output_path(self, terminal_folder: str) -> str:
             os.makedirs(output_path)
         return output_path
 
-    def convert_string_cols_to_json(
+    def _convert_string_cols_to_json(
         self, dataframe: pd.DataFrame, cols_to_modify: list[str]
     ) -> pd.DataFrame:
         """Converts values in a column from strings to JSON list
@@ -81,17 +83,16 @@ def convert_string_cols_to_json(
                 )
         return dataframe
 
-    def parse_attributes(self, save_file: bool = True) -> pd.DataFrame:
+    def parse_attributes(self, save_file: bool = True) -> Optional[str]:
         """
-        Args: save_file (bool):
-                True: merged_df is saved locally to output_path.
-                False: merged_df is returned.
+        Args:
+            save_file (bool, optional):
+              True: merged_df is saved locally to output_path.
+              False: merged_df is returned as a string
+              Defaults to True.
 
         Returns:
-            merged_df (pd.DataFrame): dataframe containing data relating to attributes
-                for the provided data model for all components in the data model.
-                Dataframe is saved locally as a csv if save_file == True, or returned if
-                save_file == False.
+            Optional[str]: if save_file=False, the dataframe as a string, otherwise None
 
         """
         # get all components
@@ -102,44 +103,51 @@ def parse_attributes(self, save_file: bool = True) -> pd.DataFrame:
         # have to provide.
         return self._parse_attributes(components, save_file)
 
-    def parse_component_attributes(
-        self, component=None, save_file: bool = True, include_index: bool = True
-    ) -> pd.DataFrame:
+    def _parse_component_attributes(
+        self,
+        component: Optional[str] = None,
+        save_file: bool = True,
+        include_index: bool = True,
+    ) -> Optional[str]:
         """
-        Args: save_file (bool):
-                True: merged_df is saved locally to output_path.
-                False: merged_df is returned.
-              include_index (bool):
-                Whether to include the index in the returned dataframe (True) or not (False)
+
+        Args:
+            component (Optional[str], optional): A component. Defaults to None.
+            save_file (bool, optional):
+              True: merged_df is saved locally to output_path.
+              False: merged_df is returned as a string
+              Defaults to True.
+            include_index (bool, optional):
+              Whether to include the index in the returned dataframe (True) or not (False)
+              Defaults to True.
+
+        Raises:
+            ValueError: If Component is None
 
         Returns:
-            merged_df (pd.DataFrame): dataframe containing data relating to attributes
-                for the provided data model for the specified component in the data model.
-                Dataframe is saved locally as a csv if save_file == True, or returned if
-                save_file == False.
+            Optional[str]: if save_file=False, the dataframe as a string, otherwise None
         """
-
         if not component:
             raise ValueError("You must provide a component to visualize.")
         return self._parse_attributes([component], save_file, include_index)
 
+    @no_type_check
     def _parse_attributes(
-        self, components: list, save_file=True, include_index=True
-    ) -> pd.DataFrame:
+        self, components: list[str], save_file: bool = True, include_index: bool = True
+    ) -> Optional[str]:
         """
         Args: save_file (bool):
                 True: merged_df is saved locally to output_path.
                 False: merged_df is returned.
-              components (list):
+              components (list[str]):
                 list of components to parse attributes for
               include_index (bool):
                 Whether to include the index in the returned dataframe (True) or not (False)
 
         Returns:
-            merged_df (pd.DataFrame): dataframe containing data relating to attributes
-                for the provided data model for specified components in the data model.
-                Dataframe is saved locally as a csv if save_file == True, or returned if
-                save_file == False.
+            Optional[str]:
+              if save_file=False, the dataframe as a string, otherwise None
+
         Raises:
             ValueError:
                 If unable hits an error while attempting to get conditional requirements.
@@ -150,6 +158,7 @@ def _parse_attributes(
         # pylint: disable=too-many-nested-blocks
         # pylint: disable=too-many-branches
         # pylint: disable=too-many-statements
+        # type
 
         # For each data type to be loaded gather all attributes the user would
         # have to provide.
@@ -264,7 +273,7 @@ def _parse_attributes(
             ]
             cols = [col for col in cols if col in data_dict_df.columns]
             data_dict_df = data_dict_df[cols]
-            data_dict_df = self.convert_string_cols_to_json(
+            data_dict_df = self._convert_string_cols_to_json(
                 data_dict_df, ["Valid Values"]
             )
             df_store.append(data_dict_df)
diff --git a/schematic/visualization/commands.py b/schematic/visualization/commands.py
index 39bedc1c8..6b302856a 100644
--- a/schematic/visualization/commands.py
+++ b/schematic/visualization/commands.py
@@ -6,13 +6,13 @@
 
 import logging
 import sys
-from typing import Any
+from typing import Any, get_args, cast
 
 import click
 import click_log  # type: ignore
 
 from schematic.visualization.attributes_explorer import AttributesExplorer
-from schematic.visualization.tangled_tree import TangledTree
+from schematic.visualization.tangled_tree import TangledTree, FigureType, TextType
 from schematic.utils.cli_utils import log_value_from_config, query_dict
 from schematic.utils.schema_utils import DisplayLabelType
 from schematic.help import viz_commands
@@ -59,7 +59,7 @@ def viz(ctx: Any, config: str) -> None:  # use as `schematic model ...`
     "--data_model_labels",
     "-dml",
     default="class_label",
-    type=click.Choice(["display_label", "class_label"], case_sensitive=True),
+    type=click.Choice(list(get_args(DisplayLabelType)), case_sensitive=True),
     help=query_dict(
         viz_commands, ("visualization", "tangled_tree", "data_model_labels")
     ),
@@ -67,12 +67,17 @@ def viz(ctx: Any, config: str) -> None:  # use as `schematic model ...`
 @click.pass_obj
 def get_attributes(
     ctx: Any,
-    data_model_labels: DisplayLabelType,
+    data_model_labels: str,
 ) -> None:
     """Gets attributes"""
     # Get JSONLD file path
     path_to_jsonld = CONFIG.model_location
     log_value_from_config("jsonld", path_to_jsonld)
+
+    # Validate inputs are literals
+    assert data_model_labels in get_args(DisplayLabelType)
+    data_model_labels = cast(DisplayLabelType, data_model_labels)
+
     # Run attributes explorer
     AttributesExplorer(path_to_jsonld, data_model_labels).parse_attributes(
         save_file=True
@@ -86,21 +91,21 @@ def get_attributes(
     "-ft",
     "--figure_type",
     required=True,
-    type=click.Choice(["component", "dependency"], case_sensitive=False),
+    type=click.Choice(list(get_args(FigureType)), case_sensitive=False),
     help=query_dict(viz_commands, ("visualization", "tangled_tree", "figure_type")),
 )
 @click.option(
     "-tf",
     "--text_format",
     required=True,
-    type=click.Choice(["plain", "highlighted"], case_sensitive=False),
+    type=click.Choice(list(get_args(TextType)), case_sensitive=False),
     help=query_dict(viz_commands, ("visualization", "tangled_tree", "text_format")),
 )
 @click.option(
     "--data_model_labels",
     "-dml",
     default="class_label",
-    type=click.Choice(["display_label", "class_label"], case_sensitive=True),
+    type=click.Choice(list(get_args(DisplayLabelType)), case_sensitive=True),
     help=query_dict(
         viz_commands, ("visualization", "tangled_tree", "data_model_labels")
     ),
@@ -110,13 +115,23 @@ def get_tangled_tree_text(
     ctx: Any,
     figure_type: str,
     text_format: str,
-    data_model_labels: DisplayLabelType,
+    data_model_labels: str,
 ) -> None:
     """Get text to be placed on the tangled tree visualization."""
     # Get JSONLD file path
     path_to_jsonld = CONFIG.model_location
     log_value_from_config("jsonld", path_to_jsonld)
 
+    # Validate inputs are literals
+    figure_type = figure_type.lower()
+    text_format = text_format.lower()
+    assert figure_type in get_args(FigureType)
+    assert text_format in get_args(TextType)
+    assert data_model_labels in get_args(DisplayLabelType)
+    figure_type = cast(FigureType, figure_type)
+    text_format = cast(TextType, text_format)
+    data_model_labels = cast(DisplayLabelType, data_model_labels)
+
     # Initialize TangledTree
     tangled_tree = TangledTree(path_to_jsonld, figure_type, data_model_labels)
 
@@ -131,14 +146,14 @@ def get_tangled_tree_text(
     "-ft",
     "--figure_type",
     required=True,
-    type=click.Choice(["component", "dependency"], case_sensitive=False),
+    type=click.Choice(list(get_args(FigureType)), case_sensitive=False),
     help=query_dict(viz_commands, ("visualization", "tangled_tree", "figure_type")),
 )
 @click.option(
     "--data_model_labels",
     "-dml",
     default="class_label",
-    type=click.Choice(["display_label", "class_label"], case_sensitive=True),
+    type=click.Choice(list(get_args(DisplayLabelType)), case_sensitive=True),
     help=query_dict(
         viz_commands, ("visualization", "tangled_tree", "data_model_labels")
     ),
@@ -147,13 +162,20 @@ def get_tangled_tree_text(
 def get_tangled_tree_component_layers(
     ctx: Any,
     figure_type: str,
-    data_model_labels: DisplayLabelType,
+    data_model_labels: str,
 ) -> None:
     """Get the components that belong in each layer of the tangled tree visualization."""
     # Get JSONLD file path
     path_to_jsonld = CONFIG.model_location
     log_value_from_config("jsonld", path_to_jsonld)
 
+    # Validate inputs are literal types
+    figure_type = figure_type.lower()
+    assert figure_type in get_args(FigureType)
+    assert data_model_labels in get_args(DisplayLabelType)
+    figure_type = cast(FigureType, figure_type)
+    data_model_labels = cast(DisplayLabelType, data_model_labels)
+
     # Initialize Tangled Tree
     tangled_tree = TangledTree(path_to_jsonld, figure_type, data_model_labels)
 
diff --git a/schematic/visualization/tangled_tree.py b/schematic/visualization/tangled_tree.py
index a91da9e17..4be2927b1 100644
--- a/schematic/visualization/tangled_tree.py
+++ b/schematic/visualization/tangled_tree.py
@@ -1,17 +1,18 @@
 """Tangled tree class"""
 
 # pylint: disable=logging-fstring-interpolation
+# pylint: disable=too-many-lines
 
 from io import StringIO
 import json
 import logging
 import os
 from os import path
-from typing import Optional, Any, Literal
+from typing import Optional, Literal, TypedDict, Union
+from typing_extensions import assert_never
 
 import networkx as nx  # type: ignore
 from networkx.classes.reportviews import NodeView, EdgeDataView  # type: ignore
-import numpy as np
 import pandas as pd
 
 from schematic.visualization.attributes_explorer import AttributesExplorer
@@ -23,6 +24,24 @@
 
 logger = logging.getLogger(__name__)
 
+FigureType = Literal["component", "dependency"]
+TextType = Literal["highlighted", "plain"]
+# A list of lists, nodes in layers
+NodeLayers = list[list[str]]
+# A dict where the keys are node names and the values are lists of node names
+NodeDict = dict[str, list[str]]
+# A dict where the keys are node names and the values are the number of a level
+NodeLevelDict = dict[str, int]
+
+
+class Node(TypedDict):
+    """Represents a node and its relationships as a dict"""
+
+    id: str
+    parents: list[str]
+    direct_children: list[str]
+    children: list[str]
+
 
 class TangledTree:  # pylint: disable=too-many-instance-attributes
     """Tangled tree class"""
@@ -30,7 +49,7 @@ class TangledTree:  # pylint: disable=too-many-instance-attributes
     def __init__(
         self,
         path_to_json_ld: str,
-        figure_type: str,
+        figure_type: FigureType,
         data_model_labels: DisplayLabelType,
     ) -> None:
         # Load jsonld
@@ -58,7 +77,7 @@ def __init__(
         self.dmge = DataModelGraphExplorer(self.graph_data_model)
 
         # Set Parameters
-        self.figure_type = figure_type.lower()
+        self.figure_type: FigureType = figure_type
         self.dependency_type = "".join(("requires", self.figure_type.capitalize()))
 
         # Get names
@@ -78,7 +97,7 @@ def __init__(
             "tangled_tree_json"
         )
 
-    def strip_double_quotes(self, string: str) -> str:
+    def _strip_double_quotes(self, string: str) -> str:
         """Removes double quotes from string
 
         Args:
@@ -95,7 +114,7 @@ def strip_double_quotes(self, string: str) -> str:
         return string
 
     def get_text_for_tangled_tree(
-        self, text_type: Literal["highlighted", "plain"], save_file: bool = False
+        self, text_type: TextType, save_file: bool = False
     ) -> Optional[str]:
         """
         Gather the text that needs to be either highlighted or plain for the
@@ -113,12 +132,14 @@ def get_text_for_tangled_tree(
         cdg = self.dmge.get_digraph_by_edge_type(self.dependency_type)
         nodes = cdg.nodes()
 
-        if self.dependency_type == "requiresComponent":
+        if self.figure_type == "component":
             component_nodes = nodes
-        else:
+        elif self.figure_type == "dependency":
             # get component nodes if making dependency figure
             component_dg = self.dmge.get_digraph_by_edge_type("requiresComponent")
             component_nodes = component_dg.nodes()
+        else:
+            assert_never(self.figure_type)
 
         # Initialize lists
         highlighted = []
@@ -133,6 +154,8 @@ def get_text_for_tangled_tree(
                 )
             elif self.figure_type == "dependency":
                 highlight_descendants = [node]
+            else:
+                assert_never(self.figure_type)
 
             # Format text to be highlighted and gather text to be formatted plain.
             if not highlight_descendants:
@@ -156,7 +179,7 @@ def get_text_for_tangled_tree(
 
         # Prepare df depending on what type of text we need.
         dataframe = pd.DataFrame(
-            locals()[text_type.lower()], columns=["Component", "type", "name"]
+            locals()[text_type], columns=["Component", "type", "name"]
         )
 
         # Depending on input either export csv locally to disk or as a string.
@@ -167,12 +190,12 @@ def get_text_for_tangled_tree(
 
         return dataframe.to_csv()
 
-    def get_topological_generations(
+    def _get_topological_generations(
         self,
-    ) -> tuple[list[list], NodeView, EdgeDataView, nx.DiGraph]:
+    ) -> tuple[NodeLayers, NodeView, EdgeDataView, nx.DiGraph]:
         """Gather topological_gen, nodes and edges based on figure type.
         Outputs:
-            topological_gen (List(list)):list of lists. Indicates layers of nodes.
+            topological_gen (NodeLayers):list of lists. Indicates layers of nodes.
             nodes: (Networkx NodeView) Nodes of the component or dependency graph.
               When iterated over it functions like a list.
             edges: (Networkx EdgeDataView) Edges of component or dependency graph.
@@ -195,9 +218,12 @@ def get_topological_generations(
             edges = rev_digraph.edges()
             topological_gen = list(nx.topological_generations(subgraph))
 
+        else:
+            assert_never(self.figure_type)
+
         return topological_gen, nodes, edges, subgraph
 
-    def remove_unwanted_characters_from_conditional_statement(
+    def _remove_unwanted_characters_from_conditional_statement(
         self, cond_req: str
     ) -> str:
         """Remove unwanted characters from conditional statement
@@ -213,7 +239,7 @@ def remove_unwanted_characters_from_conditional_statement(
             cond_req = cond_req_new.replace("If", "").lstrip().rstrip()
         return cond_req
 
-    def get_ca_alias(self, conditional_requirements: list) -> dict[str, str]:
+    def _get_ca_alias(self, conditional_requirements: list[str]) -> dict[str, str]:
         """Get the alias for each conditional attribute.
 
         NOTE: Obtaining attributes(attr) and aliases(ali) in this function is specific
@@ -227,11 +253,11 @@ def get_ca_alias(self, conditional_requirements: list) -> dict[str, str]:
                 key: alias (attribute response)
                 value: attribute
         """
-        ca_alias = {}
+        ca_alias: dict[str, str] = {}
 
         # clean up conditional requirements
         conditional_requirements = [
-            self.remove_unwanted_characters_from_conditional_statement(req)
+            self._remove_unwanted_characters_from_conditional_statement(req)
             for req in conditional_requirements
         ]
 
@@ -239,17 +265,17 @@ def get_ca_alias(self, conditional_requirements: list) -> dict[str, str]:
             if "OR" not in req:
                 attr, ali = req.split(" is ")
                 attr = "".join(attr.split())
-                ali = self.strip_double_quotes(ali)
+                ali = self._strip_double_quotes(ali)
                 ca_alias[ali] = attr
             else:
                 attr, alias_str = req.split(" is ")
                 alias_lst = alias_str.split(" OR ")
                 for elem in alias_lst:
-                    elem = self.strip_double_quotes(elem)
+                    elem = self._strip_double_quotes(elem)
                     ca_alias[elem] = attr
         return ca_alias
 
-    def gather_component_dependency_info(
+    def _gather_component_dependency_info(
         self, component_name: str, attributes_df: pd.DataFrame
     ) -> tuple[list[str], dict[str, str], list[str]]:
         """Gather all component dependency information.
@@ -276,7 +302,7 @@ def gather_component_dependency_info(
 
         # Gather conditional attributes so they can be added to the figure.
         if "Cond_Req" in attributes_df.columns:
-            conditional_attributes = list(
+            conditional_attributes: list[str] = list(
                 attributes_df[
                     (attributes_df["Cond_Req"])
                     & (attributes_df["Component"] == component_name)
@@ -288,18 +314,18 @@ def gather_component_dependency_info(
                     & (attributes_df["Component"] == component_name)
                 ]["Conditional Requirements"]
             )
-            ca_alias = self.get_ca_alias(conditional_requirements)
+            ca_alias = self._get_ca_alias(conditional_requirements)
         else:
             # If there are no conditional attributes/requirements, initialize blank lists.
             conditional_attributes = []
             ca_alias = {}
 
         # Gather a list of all attributes for the current component.
-        all_attributes = list(np.append(component_attributes, conditional_attributes))
+        all_attributes = component_attributes + conditional_attributes
 
         return conditional_attributes, ca_alias, all_attributes
 
-    def find_source_nodes(
+    def _find_source_nodes(
         self,
         nodes: NodeView,
         edges: EdgeDataView,
@@ -337,7 +363,7 @@ def find_source_nodes(
                     source_nodes.append(node)
         return source_nodes
 
-    def get_parent_child_dictionary(
+    def _get_parent_child_dictionary(
         self, edges: EdgeDataView, all_attributes: Optional[list[str]] = None
     ) -> tuple[dict[str, list[str]], dict[str, list[str]]]:
         """
@@ -360,7 +386,7 @@ def get_parent_child_dictionary(
         child_parents: dict[str, list[str]] = {}
         parent_children: dict[str, list[str]] = {}
 
-        if self.dependency_type == "requiresComponent":
+        if self.figure_type == "component":
             # Construct child_parents dictionary
             for edge in edges:
                 # Add child as a key
@@ -379,7 +405,7 @@ def get_parent_child_dictionary(
                 # Add children to list
                 parent_children[edge[1]].append(edge[0])
 
-        elif self.dependency_type == "requiresDependency":
+        elif self.figure_type == "dependency":
             # Construct child_parents dictionary
             for edge in edges:
                 # Check if child is an attribute for the current component
@@ -404,9 +430,12 @@ def get_parent_child_dictionary(
                     if edge[0] in all_attributes_list:
                         parent_children[edge[1]].append(edge[0])
 
+        else:
+            assert_never(self.dependency_type)
+
         return child_parents, parent_children
 
-    def alias_edges(self, ca_alias: dict[str, str], edges: EdgeDataView) -> list[list]:
+    def _alias_edges(self, ca_alias: dict[str, str], edges: EdgeDataView) -> NodeLayers:
         """Create new edges based on aliasing between an attribute and its response.
         Purpose:
             Create aliased edges.
@@ -427,42 +456,47 @@ def alias_edges(self, ca_alias: dict[str, str], edges: EdgeDataView) -> list[lis
             edges (Networkx EdgeDataView): Edges of component or dependency graph.
               When iterated over it works like a list of tuples.
         Output:
-            aliased_edges (list[list]) of aliased edges.
+            aliased_edges (NodeLayers) of aliased edges.
         """
-        aliased_edges = []
+        aliased_edges: NodeLayers = []
         for edge in edges:
             # construct one set of edges at a time
-            edge_set = []
+            edge_set: list[str] = []
+
+            first_edge = edge[0]
+            second_edge = edge[1]
+            assert isinstance(first_edge, str)
+            assert isinstance(second_edge, str)
 
             # If the first edge has an alias add alias to the first
             # position in the current edge set
-            if edge[0] in ca_alias.keys():
-                edge_set.append(ca_alias[edge[0]])
+            if first_edge in ca_alias.keys():
+                edge_set.append(ca_alias[first_edge])
 
             # Else add the non-aliased edge
             else:
-                edge_set.append(edge[0])
+                edge_set.append(first_edge)
 
             # If the second edge has an alias add alias to the first
             # position in the current edge set
-            if edge[1] in ca_alias.keys():
-                edge_set.append(ca_alias[edge[1]])
+            if second_edge in ca_alias.keys():
+                edge_set.append(ca_alias[second_edge])
 
             # Else add the non-aliased edge
             else:
-                edge_set.append(edge[1])
+                edge_set.append(second_edge)
 
             # Add new edge set to a the list of aliased edges.
             aliased_edges.append(edge_set)
 
         return aliased_edges
 
-    def prune_expand_topological_gen(
+    def _prune_expand_topological_gen(
         self,
-        topological_gen: list[list[str]],
+        topological_gen: NodeLayers,
         all_attributes: list[str],
         conditional_attributes: list[str],
-    ) -> list[list[str]]:
+    ) -> NodeLayers:
         """
         Purpose:
             Remake topological_gen with only relevant nodes.
@@ -472,11 +506,11 @@ def prune_expand_topological_gen(
                 attributes to topological_gen so we can visualize them in the tangled tree
                 as well.
         Input:
-            topological_gen (List[list]): Indicates layers of nodes.
-            all_attributes (list): all attributes associated with a particular component.
+            topological_gen (NodeLayers): Indicates layers of nodes.
+            all_attributes (list(str)): all attributes associated with a particular component.
             conditional_attributes (list): List of conditional attributes for a particular component
         Output:
-            new_top_gen (List[list]): mimics structure of topological_gen but only
+            new_top_gen (NodeLayers): mimics structure of topological_gen but only
                 includes the nodes we want
         """
 
@@ -506,44 +540,40 @@ def prune_expand_topological_gen(
 
         return pruned_topological_gen
 
-    def get_base_layers(
+    def _get_base_layers(
         self,
-        topological_gen: list[list],
-        child_parents: dict,
-        source_nodes: list,
+        topological_gen: NodeLayers,
+        child_parents: NodeDict,
+        source_nodes: list[str],
         component_name: str,
-    ) -> tuple[dict[str, Any], dict[str, Any]]:
+    ) -> tuple[NodeLevelDict, NodeLevelDict]:
         """
-        Purpose:
-            Reconfigure topological gen to move things back appropriate layers if
+
+        Reconfigure topological gen to move things back appropriate layers if
             they would have a back reference.
 
-            The Tangle Tree figure requires an acyclic directed graph that has additional
-                layering rules between connected nodes.
-                - If there is a backward connection then the line connecting them will
-                    break (this would suggest a cyclic connection.)
-                - Additionally if two or more nodes are connecting to a downstream node it is
-                    best to put both parent nodes at the same level, if possible, to
-                    prevent line breaks.
-                - Also want to move any children nodes one layer below
-                    the parent node(s). If there are multiple parents, put one layer below the
-                    parent that is furthest from the origin.
-
-            This is an iterative process that needs to run twice to move all the nodes to their
-            appropriate positions.
-        Input:
-            topological_gen: list of lists. Indicates layers of nodes.
-            child_parents (dict):
-                key: child
-                value: list of the child's parents
-            source_nodes: list, list of nodes that do not have a parent.
-            component_name: str, component name, default=''
-        Output:
-            base_layers: dict, key: component name, value: layer
-                represents initial layering of topological_gen
-            base_layers_copy_copy: dict, key: component name, value: layer
-                represents the final layering after moving the components/attributes to
-                their desired layer.c
+        The Tangle Tree figure requires an acyclic directed graph that has additional
+            layering rules between connected nodes.
+            - If there is a backward connection then the line connecting them will
+                break (this would suggest a cyclic connection.)
+            - Additionally if two or more nodes are connecting to a downstream node it is
+                best to put both parent nodes at the same level, if possible, to
+                prevent line breaks.
+            - Also want to move any children nodes one layer below
+                the parent node(s). If there are multiple parents, put one layer below the
+                parent that is furthest from the origin.
+
+        This is an iterative process that needs to run twice to move all the nodes to their
+        appropriate positions.
+
+        Args:
+            topological_gen (NodeLayers): Indicates layers of nodes.
+            child_parents (NodeDict): The child nodes parents
+            source_nodes (list[str]): list of nodes that do not have a parent.
+            component_name (str): component name
+
+        Returns:
+            tuple[NodeLevelDict, NodeLevelDict]: _description_
         """
         # Convert topological_gen to a dictionary
         base_layers = {com: i for i, lev in enumerate(topological_gen) for com in lev}
@@ -621,23 +651,25 @@ def get_base_layers(
 
         return base_layers, base_layers_copy_copy
 
-    def adjust_node_placement(
+    def _adjust_node_placement(
         self,
-        base_layers_copy_copy: dict[str, Any],
-        base_layers: dict[str, Any],
-        topological_gen: list[list],
-    ) -> list[list]:
-        """Reorder nodes within topological_generations to match how they were ordered in
-         base_layers_copy_copy
-        Input:
-            topological_gen: list of lists. Indicates layers of nodes.
-            base_layers: dict, key: component name, value: layer
-                represents initial layering of topological_gen
-            base_layers_copy_copy: dict, key: component name, value: layer
-                represents the final layering after moving the components/attributes to
-                their desired layer.
-        Output:
-            topological_gen: same format but as the incoming topological_gen but
+        base_layers_copy_copy: NodeLevelDict,
+        base_layers: NodeLevelDict,
+        topological_gen: NodeLayers,
+    ) -> NodeLayers:
+        """
+
+        Reorder nodes within topological_generations to match how they were ordered in
+          base_layers_copy_copy
+
+        Args:
+            base_layers_copy_copy (NodeLevelDict): represents the final layering after moving
+              the components/attributes to their desired layer.
+            base_layers (NodeLevelDict): represents initial layering of topological_gen
+            topological_gen (NodeLayers): Indicates layers of nodes.
+
+        Returns:
+            NodeLayers: same format but as the incoming topological_gen but
                 ordered to match base_layers_copy_copy.
         """
         if self.figure_type == "component":
@@ -669,18 +701,22 @@ def adjust_node_placement(
 
                     # Remove from inappropriate layer.
                     topological_gen[base_layers[node]].remove(node)
+
+        else:
+            assert_never(self.figure_type)
+
         return topological_gen
 
-    def move_source_nodes_to_bottom_of_layer(
-        self, node_layers: list[list], source_nodes: list
-    ) -> list[list]:
+    def _move_source_nodes_to_bottom_of_layer(
+        self, node_layers: NodeLayers, source_nodes: list[str]
+    ) -> NodeLayers:
         """For aesthetic purposes move source nodes to the bottom of their respective layers.
         Input:
-            node_layers (List(list)): Lists of lists of each layer and the nodes contained
+            node_layers (NodeLayers): Lists of lists of each layer and the nodes contained
               in that layer as strings.
-            source_nodes (list): list of nodes that do not have a parent.
+            source_nodes (list[str]): list of nodes that do not have a parent.
         Output:
-            node_layers (List(list)): modified to move source nodes to the bottom of each layer.
+            node_layers (NodeLayers): modified to move source nodes to the bottom of each layer.
         """
         for layer in node_layers:
             nodes_to_move = []
@@ -692,32 +728,33 @@ def move_source_nodes_to_bottom_of_layer(
                 layer.append(node)
         return node_layers
 
-    def get_layers_dict_list(
+    def _get_layers_dict_list(
         self,
-        node_layers: list[list],
-        child_parents: dict,
-        parent_children: dict,
-        all_parent_children: dict,
-    ) -> list[list[dict[str, list[str]]]]:
+        node_layers: NodeLayers,
+        child_parents: NodeDict,
+        parent_children: NodeDict,
+        all_parent_children: NodeDict,
+    ) -> list[list[Node]]:
         """Convert node_layers to a list of lists of dictionaries that specifies each node and
          its parents (if applicable).
-        Inputs:
-            node_layers: list of lists of each layer and the nodes contained in that layer
-              as strings.
-            child_parents (dict):
-                key: child
-                value: list of the child's parents
-            parent_children (dict):
-                key: parent
-                value: list of the parents children
-        Outputs:
-            layers_list (List(list): list of lists of dictionaries that specifies each node and its
+
+        Args:
+            node_layers (NodeLayers): list of lists of each layer and the nodes contained in
+              that layer as strings.
+            child_parents (NodeDict):
+              key: child
+              value: list of the child's parents
+            parent_children (NodeDict):
+              key: parent
+              value: list of the parents children
+            all_parent_children (NodeDict): _description_
+
+        Returns:
+            list[list[Node]]: list of lists of dictionaries that specifies each node and its
              parents (if applicable)
         """
         num_layers = len(node_layers)
-        layers_list: list[list[dict[str, list[str]]]] = [
-            [] for i in range(0, num_layers)
-        ]
+        layers_list: list[list[Node]] = [[] for _ in range(0, num_layers)]
         for i, layer in enumerate(node_layers):
             for node in layer:
                 if node in child_parents.keys():
@@ -734,25 +771,24 @@ def get_layers_dict_list(
                     all_children = all_parent_children[node]
                 else:
                     all_children = []
-                layers_list[i].append(
-                    {
-                        "id": node,
-                        "parents": parents,
-                        "direct_children": direct_children,
-                        "children": all_children,
-                    }
-                )
+                node_dict: Node = {
+                    "id": node,
+                    "parents": parents,
+                    "direct_children": direct_children,
+                    "children": all_children,
+                }
+                layers_list[i].append(node_dict)
 
         return layers_list
 
-    def get_node_layers_json(  # pylint: disable=too-many-arguments
+    def _get_node_layers_json(  # pylint: disable=too-many-arguments
         self,
-        topological_gen: list[list],
+        topological_gen: NodeLayers,
         source_nodes: list[str],
-        child_parents: dict,
-        parent_children: dict,
+        child_parents: NodeDict,
+        parent_children: NodeDict,
         component_name: str = "",
-        all_parent_children: Optional[dict] = None,
+        all_parent_children: Optional[NodeDict] = None,
     ) -> str:
         """Return all the layers of a single tangled tree as a JSON String.
         Inputs:
@@ -772,17 +808,17 @@ def get_node_layers_json(  # pylint: disable=too-many-arguments
             layers_json (JSON String): Layers of nodes in the tangled tree as a json string.
         """
 
-        base_layers, base_layers_copy_copy = self.get_base_layers(
+        base_layers, base_layers_copy_copy = self._get_base_layers(
             topological_gen, child_parents, source_nodes, component_name
         )
 
         # Rearrange node_layers to follow the pattern laid out in component layers.
-        node_layers = self.adjust_node_placement(
+        node_layers = self._adjust_node_placement(
             base_layers_copy_copy, base_layers, topological_gen
         )
 
         # Move source nodes to the bottom of each layer.
-        node_layers = self.move_source_nodes_to_bottom_of_layer(
+        node_layers = self._move_source_nodes_to_bottom_of_layer(
             node_layers, source_nodes
         )
 
@@ -791,7 +827,7 @@ def get_node_layers_json(  # pylint: disable=too-many-arguments
             # default to an empty dictionary
             all_parent_children = {}
 
-        layers_dicts = self.get_layers_dict_list(
+        layers_dicts = self._get_layers_dict_list(
             node_layers, child_parents, parent_children, all_parent_children
         )
 
@@ -800,26 +836,28 @@ def get_node_layers_json(  # pylint: disable=too-many-arguments
 
         return layers_json
 
-    def save_outputs(
+    def _save_outputs(
         self,
         save_file: bool,
-        layers_json,
+        layers_json: str,
         component_name: str = "",
         all_layers: Optional[list[str]] = None,
-    ) -> list[str]:
+    ) -> Union[str, list[str]]:
         """
-        Inputs:
-            save_file (bool): Indicates whether to save a file locally or not.:
-            layers_json (JSON String): Layers of nodes in the tangled tree as a json string.
-            component_name (str): component name, default=''
-            all_layers (list of json strings): Each string represents contains the layers for
-                a single tangled tree. If a dependency figure the list is added to each time
-                this function is called, so starts incomplete. default=[].
-        Outputs:
-            all_layers (list of json strings):
-                If save_file == False: Each string represents contains the layers for a single
-                 tangled tree.
-                If save_file ==True: is an empty list.
+        Args:
+            save_file (bool): Indicates whether to save a file locally or not.
+            layers_json (str): Layers of nodes in the tangled tree as a json string.
+            component_name (str, optional): component name. Defaults to "".
+            all_layers (Optional[list[str]], optional):
+                Each string represents contains the layers for a single tangled tree. If a
+                dependency figure the list is added to each time this function is called,
+                so starts incomplete. Defaults to None.
+
+        Returns:
+            Union[str, list[str]]:
+                If save_file == False: [list[str]], Each string represents contains the layers
+                  for a single tangled tree.
+                If save_file == True: str, the layers_json is returned
         """
         all_layers_list = [] if all_layers is None else all_layers
         if save_file:
@@ -845,12 +883,13 @@ def save_outputs(
                     f"{os.path.join(self.json_output_path, output_file_name)}"
                 )
             )
-            all_layers_list = layers_json
+            result: Union[str, list[str]] = layers_json
         else:
-            all_layers_list.append(layers_json)
-        return all_layers_list
+            result = all_layers_list
+            result.append(layers_json)
+        return result
 
-    def get_ancestors_nodes(
+    def _get_ancestors_nodes(
         self, subgraph: nx.DiGraph, components: list[str]
     ) -> dict[str, list[str]]:
         """
@@ -870,42 +909,47 @@ def get_ancestors_nodes(
 
         return all_parent_children
 
-    def get_tangled_tree_layers(self, save_file: bool = True):
-        """Based on user indicated figure type, construct the layers of nodes of a tangled tree.
-        Inputs:
-            save_file (bool): Indicates whether to save a file locally or not.
-        Outputs:
-            all_layers (list of json strings):
-                If save_file == False: Each string represents contains the layers
-                  for a single tangled tree.
-                If save_file ==True: is an empty list.
+    def get_tangled_tree_layers(self, save_file: bool = True) -> Union[str, list[str]]:
+        """
+
+        Based on user indicated figure type, construct the layers of nodes of a tangled tree.
 
         Note on Dependency Tangled Tree:
             If there are many conditional requirements associated with a dependency, and those
             conditional requirements have overlapping attributes associated with them
             the tangled tree will only report one
 
+        Args:
+            save_file (bool, optional):
+             Indicates whether to save a file locally or not.
+             Defaults to True.
+
+        Returns:
+            Union[str, list[str]]:
+                If save_file == False: [list[str]], Each string represents contains the layers
+                  for a single tangled tree.
+                If save_file == True: str, the layers_json is returned
         """
         # pylint: disable=too-many-locals
         # Gather the data model's, topological generations, nodes and edges
-        topological_gen, nodes, edges, subgraph = self.get_topological_generations()
+        topological_gen, nodes, edges, subgraph = self._get_topological_generations()
 
         if self.figure_type == "component":
             # Gather all source nodes
-            source_nodes = self.find_source_nodes(nodes, edges)
+            source_nodes = self._find_source_nodes(nodes, edges)
 
             # Map all children to their parents and vice versa
-            child_parents, parent_children = self.get_parent_child_dictionary(
+            child_parents, parent_children = self._get_parent_child_dictionary(
                 edges=edges
             )
 
             # find all the downstream nodes
-            all_parent_children = self.get_ancestors_nodes(
-                subgraph, parent_children.keys()
+            all_parent_children = self._get_ancestors_nodes(
+                subgraph, list(parent_children.keys())
             )
 
             # Get the layers that each node belongs to.
-            layers_json = self.get_node_layers_json(
+            layers_json = self._get_node_layers_json(
                 topological_gen,
                 source_nodes,
                 child_parents,
@@ -914,9 +958,9 @@ def get_tangled_tree_layers(self, save_file: bool = True):
             )
 
             # If indicated save outputs locally else gather all layers.
-            all_layers = self.save_outputs(save_file, layers_json)
+            all_layers = self._save_outputs(save_file, layers_json)
 
-        if self.figure_type == "dependency":
+        elif self.figure_type == "dependency":
             # Get component digraph and nodes.
             component_dg = self.dmge.get_digraph_by_edge_type("requiresComponent")
             component_nodes = component_dg.nodes()
@@ -927,36 +971,37 @@ def get_tangled_tree_layers(self, save_file: bool = True):
             )
             attributes_df = pd.read_table(StringIO(attributes_csv_str), sep=",")
 
-            all_layers = []
             for component_name in component_nodes:
                 # Gather attribute and dependency information per node
                 (
                     conditional_attributes,
                     ca_alias,
                     all_attributes,
-                ) = self.gather_component_dependency_info(component_name, attributes_df)
+                ) = self._gather_component_dependency_info(
+                    component_name, attributes_df
+                )
 
                 # Gather all source nodes
-                source_nodes = self.find_source_nodes(
+                source_nodes = self._find_source_nodes(
                     component_nodes, edges, all_attributes
                 )
 
                 # Alias the conditional requirement edge back to its actual parent label,
                 # then apply aliasing back to the edges
-                aliased_edges = self.alias_edges(ca_alias, edges)
+                aliased_edges = self._alias_edges(ca_alias, edges)
 
                 # Gather relationships between children and their parents.
-                child_parents, parent_children = self.get_parent_child_dictionary(
+                child_parents, parent_children = self._get_parent_child_dictionary(
                     aliased_edges, all_attributes
                 )
 
                 # Remake topological_gen so it has only relevant nodes.
-                pruned_topological_gen = self.prune_expand_topological_gen(
+                pruned_topological_gen = self._prune_expand_topological_gen(
                     topological_gen, all_attributes, conditional_attributes
                 )
 
                 # Get the layers that each node belongs to.
-                layers_json = self.get_node_layers_json(
+                layers_json = self._get_node_layers_json(
                     pruned_topological_gen,
                     source_nodes,
                     child_parents,
@@ -965,7 +1010,7 @@ def get_tangled_tree_layers(self, save_file: bool = True):
                 )
 
                 # If indicated save outputs locally else, gather all layers.
-                all_layers = self.save_outputs(
-                    save_file, layers_json, component_name, all_layers
-                )
+                all_layers = self._save_outputs(save_file, layers_json, component_name)
+        else:
+            assert_never(self.figure_type)
         return all_layers


Test patch:
diff --git a/.github/workflows/api_test.yml b/.github/workflows/api_test.yml
index 103b1a98d..354cbe3ba 100644
--- a/.github/workflows/api_test.yml
+++ b/.github/workflows/api_test.yml
@@ -54,17 +54,11 @@ jobs:
           key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }}
 
       #----------------------------------------------
-      # install dependencies if cache does not exist
+      # install dependencies and root project
       #----------------------------------------------
-      - name: Install dependencies
+      - name: Install dependencies and root project
         if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
-        run: poetry install --no-interaction --no-root --all-extras
-
-      #----------------------------------------------
-      #    install your root project, if required
-      #----------------------------------------------
-      - name: Install library
-        run: poetry install --no-interaction
+        run: poetry install --no-interaction --all-extras
 
       #----------------------------------------------
       #              run API test suite
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 29d1f973c..a8fff3b6d 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -66,16 +66,10 @@ jobs:
           key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }}
 
       #----------------------------------------------
-      # install dependencies if cache does not exist
+      # install dependencies and root project
       #----------------------------------------------
-      - name: Install dependencies
+      - name: Install dependencies and root project
         if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
-        run: poetry install --no-interaction --no-root --all-extras
-
-      #----------------------------------------------
-      #    install your root project, if required
-      #----------------------------------------------
-      - name: Install library
         run: poetry install --no-interaction --all-extras
 
       #----------------------------------------------
@@ -107,7 +101,7 @@ jobs:
           # add here when checked
           # poetry run mypy --install-types --non-interactive
           # add here when enforced
-          poetry run mypy --disallow-untyped-defs --install-types --non-interactive schematic/configuration/*.py schematic/exceptions.py schematic/help.py schematic/loader.py schematic/version.py
+          poetry run mypy --disallow-untyped-defs --install-types --non-interactive schematic/configuration/*.py schematic/exceptions.py schematic/help.py schematic/loader.py schematic/version.py schematic/visualization
 
       #----------------------------------------------
       #             linting
diff --git a/tests/test_viz.py b/tests/test_viz.py
index 3837620af..f40aca332 100644
--- a/tests/test_viz.py
+++ b/tests/test_viz.py
@@ -72,7 +72,7 @@ def test_ce(self, component, attributes_explorer):
         Test the output of parse_component_attributes
         """
         # get attributes string
-        component_attributes_str = attributes_explorer.parse_component_attributes(
+        component_attributes_str = attributes_explorer._parse_component_attributes(
             component=component, save_file=False, include_index=False
         )
         # convert to dataframe