Repository: ansys/pydyna

Issue:
Add include file name to keywords while expanding the deck using deck.expand()
### 📝 Description of the feature

Add include filename as an attribute to the keywords. This would be really helpful to quickly identify in which file the specific keyword is defined. 


### 💡 Steps for implementing the feature

This can be achieved by automatically adding the include file name as an attribute to the keywords while reading (expanding) the using deck.expand(). 


### 🔗 Useful links and references

_No response_

Discussion:
@koubaa: Hello - this is straightforward to do. I will get it to it as soon as I can.

Solution patch:
diff --git a/doc/changelog/768.miscellaneous.md b/doc/changelog/768.miscellaneous.md
new file mode 100644
index 00000000..ac498032
--- /dev/null
+++ b/doc/changelog/768.miscellaneous.md
@@ -0,0 +1 @@
+feat: add include name property to keyword
\ No newline at end of file
diff --git a/src/ansys/dyna/core/lib/deck.py b/src/ansys/dyna/core/lib/deck.py
index 2a1689e5..bf9a9f79 100644
--- a/src/ansys/dyna/core/lib/deck.py
+++ b/src/ansys/dyna/core/lib/deck.py
@@ -218,33 +218,32 @@ class Deck:
                 search_paths.append(keyword.filename)
                 keywords.append(keyword)
                 continue
-            success = False
+            expand_include_file = None
             for search_path in search_paths:
                 include_file = os.path.join(search_path, keyword.filename)
-                if not os.path.isfile(include_file):
-                    continue
-                xform = None
-                if keyword.subkeyword == "TRANSFORM":
-                    xform = keyword
-                context = ImportContext(xform, self, include_file)
-                try:
-                    include_deck = self._prepare_deck_for_expand(keyword)
-                    include_deck._import_file(include_file, "utf-8", context)
-                except UnicodeDecodeError as e:
-                    encoding = self._detect_encoding(include_file)
-                    include_deck = self._prepare_deck_for_expand(keyword)
-                    include_deck._import_file(include_file, encoding, context)
-                success = True
-                break
-            if success:
-                if recurse:
-                    # TODO: merge the parameters if the "LOCAL" option is not used!
-                    expanded = include_deck._expand_helper(search_paths, True)
-                    keywords.extend(expanded)
-                else:
-                    keywords.extend(include_deck.all_keywords)
-            else:
+                if os.path.isfile(include_file):
+                    expand_include_file = include_file
+                    break
+            if expand_include_file is None:
                 keywords.append(keyword)
+                continue
+            xform = None
+            if keyword.subkeyword == "TRANSFORM":
+                xform = keyword
+            context = ImportContext(xform, self, expand_include_file)
+            try:
+                include_deck = self._prepare_deck_for_expand(keyword)
+                include_deck._import_file(expand_include_file, "utf-8", context)
+            except UnicodeDecodeError as e:
+                encoding = self._detect_encoding(expand_include_file)
+                include_deck = self._prepare_deck_for_expand(keyword)
+                include_deck._import_file(expand_include_file, encoding, context)
+            if recurse:
+                # TODO: merge the parameters if the "LOCAL" option is not used!
+                expanded = include_deck._expand_helper(search_paths, True)
+                keywords.extend(expanded)
+            else:
+                keywords.extend(include_deck.all_keywords)
         for keyword in keywords:
             if isinstance(keyword, KeywordBase):
                 keyword.deck = None
@@ -505,7 +504,10 @@ class Deck:
         from ansys.dyna.core.lib.deck_loader import load_deck_from_buffer
 
         with open(path, encoding=encoding) as f:
-            return load_deck_from_buffer(self, f, context, self._import_handlers)
+            loader_result = load_deck_from_buffer(self, f, context, self._import_handlers)
+        for keyword in self.keywords:
+            keyword.included_from = path
+        return loader_result
 
     def import_file(
         self, path: str, encoding: str = "utf-8"
diff --git a/src/ansys/dyna/core/lib/keyword_base.py b/src/ansys/dyna/core/lib/keyword_base.py
index da2f365c..e8010063 100644
--- a/src/ansys/dyna/core/lib/keyword_base.py
+++ b/src/ansys/dyna/core/lib/keyword_base.py
@@ -48,6 +48,7 @@ class KeywordBase(Cards):
         self.user_comment = kwargs.get("user_comment", "")
         self._format_type: format_type = kwargs.get("format", format_type.default)
         self._deck = None
+        self._included_from = None
 
     @property
     def deck(self) -> typing.Optional["Deck"]:
@@ -109,6 +110,19 @@ class KeywordBase(Cards):
         """Gets the cards of the keyword"""
         return self._get_all_cards()
 
+    @property
+    def included_from(self) -> str:
+        """Get the filename this was included from.
+
+        If the keyword was not read from a file,
+        return None.
+        """
+        return self._included_from
+
+    @included_from.setter
+    def included_from(self, value: str):
+        self._included_from = value
+
     def _get_user_comment_lines(self) -> typing.List[str]:
         user_comment = self.user_comment
         if user_comment == "":


Test patch:
diff --git a/tests/test_deck.py b/tests/test_deck.py
index 0183f3f4..cf888409 100644
--- a/tests/test_deck.py
+++ b/tests/test_deck.py
@@ -100,6 +100,7 @@ def test_deck_003(file_utils):
     assert import_handler._num_keywords == 12
     assert len(deck.keywords) == 11
     assert len(deck.all_keywords) == 11
+    assert deck.keywords[0].included_from == filepath
     assert deck.get(type="ALE")[0].nid1 == 1
 
 
@@ -178,6 +179,7 @@ def test_deck_copy():
     x.append(kwd.Mat001(mid=99))
     y = copy.deepcopy(x)
     y.keywords[0].mid = 100
+    assert y.keywords[0].included_from is None
     assert x.keywords[0].mid == 99
     assert y.keywords[0].mid == 100
 
@@ -396,10 +398,12 @@ def test_deck_expand(file_utils):
     """Test that a long deck can read a standard deck."""
     # I think there are more corner cases related to this to iron out..
     deck = Deck(format=format_type.long)
+    include_path = file_utils.get_asset_file_path("test.k")
     deck.append(kwd.Include(filename=file_utils.get_asset_file_path("test.k"), format=format_type.standard))
     expanded_deck = deck.expand()
     assert len(expanded_deck.keywords) == 12
     assert expanded_deck.keywords[1].format == format_type.standard
+    assert expanded_deck.keywords[0].included_from == include_path
 
 
 @pytest.mark.keywords