Repository: DevOps-MBSE/AaC
Pull request #980: File Error Handling.
Author: login: bunchw
Labels: name: changed
description: For when PR includes information in the changed category
Created: 2025-03-10T15:37:07Z | Merged: 2025-03-11T17:33:09Z

# Description

File Error Handling.

# Linked Items:

Closes/Fixes/Resolves #961 

### Added

- _Describe any new features._

### Changed

Added some try/catch blocks to file opening, and removed two unused files.

### Deprecated

- _Describe any deprecated features._

### Removed

- _Describe any removed features._

### Fixed

- _Describe any bug fixes._

### Security

- _Describe any security-related changes._

# Checklist:

- [ ] I updated project documentation to reflect my changes.
- [x] My changes generate no new warnings.
- [ ] I updated new and existing unit tests to account for my changes.
- [x] I linked the associated item(s) to be closed.
- [x] I bumped the version.
- [x] I added the labels corresponding to my changes.


Linked issue #961: 📋 [TASK] - File Error Handling
### Task Description

As an AaC Developer, I would like to add error handling around file sanitization, so that I have improved error handling for file related issues. 

### Acceptance Criteria

- [ ] Error handling is incorporated with the sanitization portion of file handling
- [ ] Unit tests for sanitization need to be expanded to exercise the new error handling

### Unresolved questions

_No response_

### Additional Resources

_No response_

Diff:
diff --git a/python/src/aac/__init__.py b/python/src/aac/__init__.py
index 1c1282316..e6c5ba22b 100644
--- a/python/src/aac/__init__.py
+++ b/python/src/aac/__init__.py
@@ -12,7 +12,7 @@
 import logging
 import os
 
-__version__ = "0.5.19"
+__version__ = "0.5.20"
 
 __log_file_name__ = os.path.join(os.path.dirname(__file__), "aac.log")
 
diff --git a/python/src/aac/in_out/parser/_parse_source.py b/python/src/aac/in_out/parser/_parse_source.py
index f540aa02e..59b18113a 100644
--- a/python/src/aac/in_out/parser/_parse_source.py
+++ b/python/src/aac/in_out/parser/_parse_source.py
@@ -164,6 +164,9 @@ def _read_arch_file_content(arch_file: str) -> str:
 
     Returns:
         The contents of the file as a string.
+    Exceptions:
+        IOError: Exception raised when the file path cannot be parsed.
+        Exception: Generic exception raised when an unexpected error is encountered.
     """
     content = ""
     acceptable_file_extensions = [YAML_DOCUMENT_EXTENSION, AAC_DOCUMENT_EXTENSION]
@@ -175,7 +178,9 @@ def _read_arch_file_content(arch_file: str) -> str:
                 content = file.read()
         except IOError as error:
             logging.error(f"Failed to parse {arch_file} with error {error}")
-
+        except Exception as error:
+            # Catch-all for any unknown and unexpected errors with opening and reading files.
+            logging.error(f"Unexpected error when opening {arch_file} with {error}")
         if not content:
             logging.error(f"Failed to parse {arch_file}, it's an empty file.")
     else:
diff --git a/python/src/aac/in_out/writer.py b/python/src/aac/in_out/writer.py
deleted file mode 100644
index 480c58071..000000000
--- a/python/src/aac/in_out/writer.py
+++ /dev/null
@@ -1,65 +0,0 @@
-"""AaC Module for easy file writing."""
-import sys
-import logging
-from os import path, makedirs
-
-from aac.context.definition import Definition
-from aac.in_out.constants import DEFINITION_SEPARATOR
-from aac.in_out.paths import sanitize_filesystem_path
-
-
-def write_file(uri: str, content: str, overwrite: bool = False) -> None:
-    """
-    Write string content to a file.
-
-    Args:
-        uri (str): the file's full uri
-        content (str): contents of the file to write
-        overwrite (bool): True to overwrite an existing file or false to not.
-    """
-    sanitized_uri = sanitize_filesystem_path(uri)
-    does_file_exist = path.exists(sanitized_uri)
-    file_parent_dir = path.dirname(sanitized_uri)
-
-    if not path.exists(file_parent_dir):
-        makedirs(file_parent_dir, exist_ok=True)
-
-    if not overwrite and does_file_exist:
-        logging.info(f"{sanitized_uri} already exists, skipping write.")
-        return
-
-    try:
-        with open(sanitized_uri, "w") as file:
-            file.writelines(content)
-    except IOError as error:
-        logging.error(f"Failed to write file {sanitized_uri} due to error: {error}")
-
-
-def write_definitions_to_file(definitions: list[Definition], file_uri: str, is_user_editable: bool = True) -> None:
-    """
-    Given a list of definitions, write them to file uri. Updates the source for definitions passed in.
-
-    Args:
-        definitions (list[Definition]): The definitions to write to file.
-        file_uri (str): The URI of the file to write the definitions to.
-        is_user_editable (bool): True if the AaC file can be edited by users.
-    """
-    def sort_definitions_by_lexeme_line(definition: Definition) -> int:
-        line = sys.maxsize
-
-        if definition.lexemes and definition.lexemes[0].source == file_uri:
-            line = definition.lexemes[0].location.line
-
-        return line
-
-    definitions.sort(key=sort_definitions_by_lexeme_line)
-
-    file_content = ""
-    for definition in definitions:
-        definition.source.uri = file_uri
-        definition.source.is_user_editable = is_user_editable
-
-        yaml_doc_separator = DEFINITION_SEPARATOR if file_content else ""
-        file_content += f"{yaml_doc_separator}{definition.to_yaml()}"
-
-    write_file(file_uri, file_content, True)
diff --git a/python/src/aac/plugins/generate/generate_impl.py b/python/src/aac/plugins/generate/generate_impl.py
index 42044e864..08206775b 100644
--- a/python/src/aac/plugins/generate/generate_impl.py
+++ b/python/src/aac/plugins/generate/generate_impl.py
@@ -1,5 +1,6 @@
 """AaC Plugin implementation module for the Version plugin."""
 
+import logging
 from os import path, makedirs, walk, remove
 import importlib
 from typing import Callable, Any
@@ -16,6 +17,7 @@
 from aac.context.language_context import LanguageContext
 from aac.context.definition import Definition
 from aac.in_out.parser._parse_source import parse
+from aac.in_out.paths import sanitize_filesystem_path
 from aac.plugins.generate.helpers.python_helpers import (
     get_path_from_package,
     get_python_name,
@@ -24,6 +26,29 @@
 plugin_name = "Generate"
 
 
+def _write_to_file(file_path: str, content: str):
+    """
+    Opens, writes to, and closes a file.
+
+    Args:
+        file_path (str): File path to be written to.
+        content (str): Content to be written to file.
+
+    Exceptions:
+        IOError: Exception raised when the file path cannot be parsed.
+        Exception: Generic exception raised when an unexpected error is encountered.
+    """
+    try:
+        with open(file_path, "w") as output_file:
+            output_file.write(content)
+            output_file.close()
+    except IOError as error:
+        logging.error(f"Failed to parse {file_path} with error {error}")
+    except Exception as error:
+        # Catch-all for any unknown and unexpected errors with opening and reading files.
+        logging.error(f"Unexpected error when opening {file_path} with {error}")
+
+
 def output_to_jinja_template(
     evaluate: bool,
     force_overwrite: bool,
@@ -80,10 +105,7 @@ def output_to_jinja_template(
 
     if evaluate:
         # write contents to an aac_evaluate file for user review
-        evaluate_file_path = f"{output_file_path}.aac_evaluate"
-        with open(evaluate_file_path, "w") as output_file:
-            output_file.write(output)
-            output_file.close()
+        _write_to_file(f"{output_file_path}.aac_evaluate", output)
     else:
         # write contents to output_file_path
         if force_overwrite or template.overwrite in [
@@ -93,9 +115,7 @@ def output_to_jinja_template(
         ]:
             if path.exists(output_file_path):
                 backup_file(output_file_path)
-            with open(output_file_path, "w") as output_file:
-                output_file.write(output)
-                output_file.close()
+            _write_to_file(output_file_path, output)
         elif template.overwrite in [
             context.create_aac_enum(
                 "aac.lang.OverwriteOption", "SKIP"
@@ -103,16 +123,12 @@ def output_to_jinja_template(
         ]:
             # this is for the skip option, so only write if file doesn't exist
             if not path.exists(output_file_path):
-                with open(output_file_path, "w") as output_file:
-                    output_file.write(output)
-                    output_file.close()
+                _write_to_file(output_file_path, output)
             else:
                 evaluate_file_path = (
                     f"{output_file_path}.aac_evaluate"
                 )
-                with open(evaluate_file_path, "w") as output_file:
-                    output_file.write(output)
-                    output_file.close()
+                _write_to_file(evaluate_file_path, output)
 
 
 def generate_content(
@@ -258,6 +274,9 @@ def generate(
 
     Returns:
         ExecutionResult: The result of executing the generate command.
+
+    Raises:
+        OperationCancelled: When the operation is canceled by the user.
     """
 
     result = ExecutionResult(plugin_name, "generate", ExecutionStatus.SUCCESS, [])
@@ -306,8 +325,22 @@ def generate(
 def clean(
     aac_file: str, code_output: str, test_output: str, doc_output: str, no_prompt: bool
 ) -> ExecutionResult:
-    """Clean up generated code, tests, and docs."""
+    """
+    Clean up generated code, tests, and docs.
+
+    Args:
+        aac_file (str):  Content used for generating the target files.
+        code_out_dir (str): Output path for target code files.
+        test_out_dir (str): Output path for target test files.
+        doc_out_dir (str): Output path for target doc files.
+        no_prompt (bool): Command argument.  If true, executes with no prompt.  Primarily used for testing or by other plugins.
+
+    Returns:
+        ExecutionResult: The result of executing the clean command.
+    """
+
     # setup directories
+
     code_out_dir, test_out_dir, doc_out_dir = get_output_directories(
         "AaC will delete backup and eval files in the following directories:",
         aac_file,
@@ -335,7 +368,24 @@ def get_output_directories(
     doc_output: str,
     no_prompt: bool,
 ) -> tuple[str, str, str]:
-    """Returns the output directories for code, tests, and docs."""
+    """
+    Returns the output directories for code, tests, and docs.
+
+    Args:
+        message (str): Output message.
+        aac_plugin_file (str):  Content to be used for generation.
+        code_output (str): Output path for generated code files.
+        test_output (str): Output path for generated test files.
+        doc_output (str): Output path for generated doc files.
+        no_prompt (bool): Command argument.  If true, generates files with no prompt.  Primarily used for testing or by other plugins.
+
+    Returns:
+        tuple[str, str, str]: A tuple containing the three output paths.
+
+    Raises:
+        ExecutionError: When the content of the data source file is invalid.
+        OperationCancelled: When the operation is canceled by the user.
+    """
     code_out: str = code_output
     test_out: str = test_output
     doc_out: str = doc_output
@@ -378,6 +428,10 @@ def get_output_directories(
         else:
             raise OperationCancelled("User cancelled operation.")
 
+    code_out = sanitize_filesystem_path(code_out)
+    test_out = sanitize_filesystem_path(test_out)
+    doc_out = sanitize_filesystem_path(doc_out)
+
     return (code_out, test_out, doc_out)
 
 
@@ -387,7 +441,18 @@ def get_output_file_path(
     source_package: str,
     source_name: str,
 ) -> str:
-    """Returns the output file path for a generator template."""
+    """
+    Returns the output file path for a generator template.
+
+    Args:
+        root_output_directory (str): Root directory of the output path.
+        generator_template: Template for the aac generator file.
+        source_package (str): Package containing the definition source.
+        source_name (str): Name of the definition.
+
+    Returns:
+        str: Output file path for a generator template.
+    """
     result = root_output_directory
     if generator_template.output_path_uses_data_source_package and source_package:
         context = LanguageContext()
@@ -410,11 +475,21 @@ def get_output_file_path(
         file_name = f"{file_name}{generator_template.output_file_suffix}"
     file_name = f"{file_name}.{generator_template.output_file_extension}"
     result = path.join(result, file_name)
-    return path.abspath(result)
+    return sanitize_filesystem_path(path.abspath(result))
 
 
 def get_callable(package_name: str, file_name: str, function_name: str) -> Callable:
-    """Returns a callable function from a package, file, and function name."""
+    """
+    Returns a callable function from a package, file, and function name.
+
+    Args:
+        package_name (str): Package name where the function resides.
+        file_name (str): File name where the function resides.
+        function_name (str): Name of the Function.
+
+    Returns:
+        Callable: Callable form of the function.
+    """
     module = importlib.import_module(f"{package_name}.{file_name}")
     return getattr(module, function_name)
 
@@ -422,7 +497,16 @@ def get_callable(package_name: str, file_name: str, function_name: str) -> Calla
 def load_template(
     template_abs_path: str, helper_functions: dict[str, Callable] = {}
 ) -> Template:
-    """Load a jinja2 template from a file."""
+    """
+    Load a jinja2 template from a file.
+
+    Args:
+        template_abs_path (str): Absolute Path of the Jinja2 Template.
+        helper_functions (dict[str, Callable]): Helper Functions for the template.
+
+    Returns:
+        template (Template): Jinja2 Template object.
+    """
     env = Environment(loader=FileSystemLoader("/"))
     env.globals.update(helper_functions)
     template = env.get_template(template_abs_path)
@@ -430,9 +514,29 @@ def load_template(
 
 
 def backup_file(file_path: str) -> str:
-    """Backs up a file by appending .aac_backup to the file name."""
+    """
+    Backs up a file by appending .aac_backup to the file name.
+
+    Args:
+        file_path (str): The file being backed up.
+
+    Returns:
+        The file path to the backup file.
+
+    Exceptions:
+        IOError: Exception raised when the file path cannot be parsed.
+        Exception: Generic exception raised when an unexpected error is encountered.
+    """
     backup_file_path = f"{file_path}.aac_backup"
-    with open(file_path, "r") as input_file:
-        with open(backup_file_path, "w") as backup_file:
-            backup_file.write(input_file.read())
-    return backup_file_path
+    try:
+        with open(file_path, "r") as input_file:
+            with open(backup_file_path, "w") as backup_file:
+                backup_file.write(input_file.read())
+                backup_file.close()
+            input_file.close()
+    except IOError as error:
+        logging.error(f"Failed to parse {file_path} with error {error}")
+    except Exception as error:
+        # Catch-all for any unknown and unexpected errors with opening and reading files.
+        logging.error(f"Unexpected error when opening {file_path} with {error}")
+    return sanitize_filesystem_path(backup_file_path)
diff --git a/python/tests/test_aac/in_out/test_writer.py b/python/tests/test_aac/in_out/test_writer.py
deleted file mode 100644
index 1ff3dcce1..000000000
--- a/python/tests/test_aac/in_out/test_writer.py
+++ /dev/null
@@ -1,59 +0,0 @@
-import os
-from tempfile import TemporaryDirectory
-from unittest import TestCase
-
-from aac.in_out.parser import parse
-from aac.in_out.writer import write_definitions_to_file
-from aac.context.language_context import LanguageContext
-
-class TestWriter(TestCase):
-    
-    def test_write_definitions_to_file(self):
-        test_file_name = "out.yaml"
-
-        test_model_name = "TestModel"
-        test_model_yaml = """
-model:
-  name: TestModel
-  description: This is a test model with no content.
-""".strip()
-        test_enum_name = "TestEnum"
-        test_enum_yaml = """
-enum:
-  name: TestEnum
-  values:
-  - v1
-""".strip()
-
-        with TemporaryDirectory() as temp_dir:
-            test_model = parse(test_model_yaml)[0]
-            test_enum = parse(test_enum_yaml)[0]
-            test_file_uri = os.path.join(temp_dir, test_file_name)
-            write_definitions_to_file([test_model, test_enum], test_file_uri)
-
-            self.assertTrue(os.path.exists(test_file_uri))
-
-            with open(test_file_uri, "r") as file:
-                actual_result = file.read()
-                self.assertIn(test_model_yaml, actual_result)
-                self.assertIn("---", actual_result)
-                self.assertIn(test_enum_yaml, actual_result)
-
-    def test_write_definitions_to_file_preserve_order(self):
-        test_file_name = "out.yaml"
-
-        context = LanguageContext()
-        test_definitions = context.get_aac_core_definitions()
-
-        with TemporaryDirectory() as temp_dir:
-            test_file_uri = os.path.join(temp_dir, test_file_name)
-            write_definitions_to_file(test_definitions, test_file_uri)
-
-            self.assertTrue(os.path.exists(test_file_uri))
-
-            actual_result_parsed_definitions = parse(test_file_uri)
-
-            self.assertListEqual([d.name for d in test_definitions], [d.name for d in actual_result_parsed_definitions])
-            self.assertListEqual([d.source.uri for d in test_definitions], [d.source.uri for d in actual_result_parsed_definitions])
-            # Disabling this test for now since the writer doesn't maintain multiline descriptions correctly which causes inconsistent lexeme lines when written to file then read back in.
-            # self.assertListEqual([d.lexemes[0].location.line for d in test_definitions], [d.lexemes[0].location.line for d in actual_result_parsed_definitions]) # noqa E800


Reviews:
user: login: lizzcondrey
body: Also need to expand testing to confirm these are seen when hit. And update method docstrings to include the additional handled generic exception. 

And don't see the adds of sanitization for being leveraged against provided output paths, was that already in place and we misread last week? 

And don't see adds for errors around the sanitization calls, found not to be a break spot?
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-10T15:46:07Z
user: login: lizzcondrey
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-10T20:25:32Z
user: login: lizzcondrey
body: A couple docstring tweaks and looks like a close file got missed. 

And need to merge in main. 
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-11T15:56:19Z
user: login: jasoncbryant
body: I suggest adding a Raises block to generate_impl.py get_output_directories since it can raises both ExecutionError and OperationCancelled.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T16:12:34Z
user: login: jasoncbryant
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T16:24:48Z
user: login: lizzcondrey
body: Still need to bump version following merge of main
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-11T16:27:48Z
user: login: lizzcondrey
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-11T17:00:58Z
user: login: lizzcondrey
body: That should be everything, good job getting this modified to provide some additional handling around files while we continue to work through the intermittent behaviors. 
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-11T17:07:37Z

Review comments:
diff_hunk: @@ -175,7 +175,9 @@ def _read_arch_file_content(arch_file: str) -> str:
                 content = file.read()
         except IOError as error:
             logging.error(f"Failed to parse {arch_file} with error {error}")
-
+        except Exception as error:
+            # Catch-all for any unknown and unexpected errors with opening and reading files.
+            print(f"Unexpected error when opening {arch_file} with {error}")
path: python/src/aac/in_out/parser/_parse_source.py
user: login: lizzcondrey
body: So, this shouldn't be a print statement, but a logging entry and potentially an ExecutionResult, though I don't think we return ExecutionResults from this layer usually, but the one above it? Not looked to confirm yet. 
created_at: 2025-03-10T15:42:05Z
author_association: COLLABORATOR
diff_hunk: @@ -432,7 +432,13 @@ def load_template(
 def backup_file(file_path: str) -> str:
     """Backs up a file by appending .aac_backup to the file name."""
     backup_file_path = f"{file_path}.aac_backup"
-    with open(file_path, "r") as input_file:
-        with open(backup_file_path, "w") as backup_file:
-            backup_file.write(input_file.read())
+    try:
+        with open(file_path, "r") as input_file:
+            with open(backup_file_path, "w") as backup_file:
+                backup_file.write(input_file.read())
+    except IOError as error:
+        print(f"Failed to parse {file_path} with error {error}")
+    except Exception as error:
+        # Catch-all for any unknown and unexpected errors with opening and reading files.
+        print(f"Unexpected error when opening {file_path} with {error}")
path: python/src/aac/plugins/generate/generate_impl.py
user: login: lizzcondrey
body: Here as well, they shouldn't be print statements but logged statements and ExecutionResults since ExecutionResults are returned from within generate.
created_at: 2025-03-10T15:43:53Z
author_association: COLLABORATOR
diff_hunk: @@ -430,9 +436,27 @@ def load_template(
 
 
 def backup_file(file_path: str) -> str:
-    """Backs up a file by appending .aac_backup to the file name."""
+    """
path: python/src/aac/plugins/generate/generate_impl.py
user: login: lizzcondrey
body: Docstrings should be improved in entirety of file. And there are other methods that contain opening of files that similar handling should be added around still. 
created_at: 2025-03-10T20:25:24Z
author_association: COLLABORATOR
diff_hunk: @@ -306,7 +318,17 @@ def generate(
 def clean(
     aac_file: str, code_output: str, test_output: str, doc_output: str, no_prompt: bool
 ) -> ExecutionResult:
-    """Clean up generated code, tests, and docs."""
+    """
path: python/src/aac/plugins/generate/generate_impl.py
user: login: lizzcondrey
body: Looks like the description got dropped.
```suggestion
    """
    Clean up generated code, tests, and docs.
```
created_at: 2025-03-11T15:46:29Z
author_association: COLLABORATOR
diff_hunk: @@ -387,7 +426,18 @@ def get_output_file_path(
     source_package: str,
     source_name: str,
 ) -> str:
-    """Returns the output file path for a generator template."""
+    """
+    Returns the output file path for a generator template.
+
+    Args:
+        root_output_directory (str): Root directory of the output path.
+        generator_template: Template for the aac generator file.
+        source_package (str): Output directory path.
+        source_name (str): Name of the Python type.
path: python/src/aac/plugins/generate/generate_impl.py
user: login: lizzcondrey
body: ```suggestion
        source_package (str): Package containing the definition source.
        source_name (str): Name of the definition.
```
created_at: 2025-03-11T15:49:56Z
author_association: COLLABORATOR
diff_hunk: @@ -24,6 +26,25 @@
 plugin_name = "Generate"
 
 
+def _write_to_file(file_path: str, content: str):
+    """
+    Opens, writes to, and closes a file.
+
+    Args:
+        file_path (str): File path to be written to.
+        content (str): Content to be written to file.
+    """
path: python/src/aac/plugins/generate/generate_impl.py
user: login: lizzcondrey
body: Include the Exceptions
created_at: 2025-03-11T15:51:49Z
author_association: COLLABORATOR
diff_hunk: @@ -410,29 +475,66 @@ def get_output_file_path(
         file_name = f"{file_name}{generator_template.output_file_suffix}"
     file_name = f"{file_name}.{generator_template.output_file_extension}"
     result = path.join(result, file_name)
-    return path.abspath(result)
+    return sanitize_filesystem_path(path.abspath(result))
 
 
 def get_callable(package_name: str, file_name: str, function_name: str) -> Callable:
-    """Returns a callable function from a package, file, and function name."""
+    """
+    Returns a callable function from a package, file, and function name.
+
+    Args:
+        package_name (str): Package name where the function resides.
+        file_name (str): File name where the function resides.
+        function_name (str): Name of the Function.
+
+    Returns:
+        Callable: Callable form of the function.
+    """
     module = importlib.import_module(f"{package_name}.{file_name}")
     return getattr(module, function_name)
 
 
 def load_template(
     template_abs_path: str, helper_functions: dict[str, Callable] = {}
 ) -> Template:
-    """Load a jinja2 template from a file."""
+    """
+    Load a jinja2 template from a file.
+
+    Args:
+        template_abs_path (str): Absolute Path of the Jinja2 Template.
+        helper_functions (dict[str, Callable]): Helper Functions for the template.
+
+    Returns:
+        template (Template): Jinja2 Template object.
+    """
     env = Environment(loader=FileSystemLoader("/"))
     env.globals.update(helper_functions)
     template = env.get_template(template_abs_path)
     return template
 
 
 def backup_file(file_path: str) -> str:
-    """Backs up a file by appending .aac_backup to the file name."""
+    """
+    Backs up a file by appending .aac_backup to the file name.
+
+    Args:
+        file_path (str): The file being backed up.
+
+    Returns:
+        The file path to the backup file.
+
+    Exceptions:
+        IOError: Exception raised when the file path cannot be parsed.
+        Exception: Generic exception raised when an unexpected error is encountered.
+    """
     backup_file_path = f"{file_path}.aac_backup"
-    with open(file_path, "r") as input_file:
-        with open(backup_file_path, "w") as backup_file:
-            backup_file.write(input_file.read())
-    return backup_file_path
+    try:
+        with open(file_path, "r") as input_file:
+            with open(backup_file_path, "w") as backup_file:
+                backup_file.write(input_file.read())
path: python/src/aac/plugins/generate/generate_impl.py
user: login: lizzcondrey
body: Not sure what happened, maybe missed hitting publish of the comment because I can't find it in previously submitted ones, but looks like we are missing closing the files here. 
created_at: 2025-03-11T17:00:49Z
author_association: COLLABORATOR

PR comments:
user: login: bunchw
created_at: 2025-03-10T15:39:09Z
body: So, I wasn't able to find much in terms of places that need error checking, so I added a couple of "catch-all" places that catch any unknown and unexpected errors.  I also removed the two unused writer files.
author_association: COLLABORATOR
user: login: lizzcondrey
created_at: 2025-03-10T15:55:21Z
body: And looking over the whole of generate_impl, there are many other file handling locations throughout that aren't being wrapped for if an IOError or Exception occur. 
author_association: COLLABORATOR