From 28fb1683b99e6ab95583c51c29727de5d02f55fb Mon Sep 17 00:00:00 2001 From: Josh Ayers Date: Sun, 1 Oct 2023 10:41:22 -0700 Subject: [PATCH] Bug fix include rel paths (#28) * Fix an inconsistency with Nastran when following include statements. If an INCLUDE statement only specifies a filename (i.e. INCLUDE 'file.bdf'), Nastran treats file.bdf as if it's in the same directory as the file with the INCLUDE statement. If an INCLUDE statement includes path components (i.e. INCLUDE 'dir1/file.bdf' or INCLUDE '../file.bdf') Nastran treats the path as if it's relative to the directory containing the root Nastran input deck. Previously, rdcards and rdsets always used the second method. This was fixed. * Make a copy of kwargs dictionary before recursive call. * Correct docstring. --------- Co-authored-by: Josh Ayers --- pyyeti/nastran/bulk.py | 49 +++++++++++++++++++++++------------- pyyeti/tests/test_nastran.py | 49 +++++++++++++++++++++++++++++++++++- 2 files changed, 80 insertions(+), 18 deletions(-) diff --git a/pyyeti/nastran/bulk.py b/pyyeti/nastran/bulk.py index 05d2d92..26b81ac 100644 --- a/pyyeti/nastran/bulk.py +++ b/pyyeti/nastran/bulk.py @@ -416,9 +416,18 @@ def _rdinclude(fiter, s, func, kwargs): else: # end not found, add entire line path_parts.append(s.strip()) rel_path = "".join(path_parts) + current_dir_path, root_dir_path = kwargs["include_root_dirs"] path, symbol_found = _check_for_symbols(rel_path, kwargs["include_symbols"]) if not symbol_found: - path = os.path.abspath(os.path.join(kwargs["include_root_dir"], rel_path)) + if os.path.dirname(rel_path) == "": + # rel_path is just a filename, it is relative to current file + dir_path = current_dir_path + else: + # rel_path includes directories, it is relative to root directory + dir_path = root_dir_path + path = os.path.abspath(os.path.join(dir_path, rel_path)) + kwargs = kwargs.copy() # make a copy for recursive call + kwargs["include_root_dirs"] = (os.path.dirname(path), root_dir_path) # read next line, which will be returned by next fiter.send(True) call fiter.send(False) return func(path, **kwargs) @@ -555,7 +564,7 @@ def rdcards( keep_comments=False, follow_includes=True, include_symbols=None, - include_root_dir=None, + include_root_dirs=None, ): r""" Read Nastran cards (lines) into a matrix, dictionary, or list. @@ -626,7 +635,7 @@ def rdcards( include_symbols : dict; optional A dictionary mapping Nastran symbols to an associated path. These can be read from a file using :func:`rdsymbols`. - include_root_dir : None; optional + include_root_dirs : None; optional This parameter is only used when this function is called recursively while following INCLUDE statements. Users should keep it as the default value of None. @@ -741,15 +750,18 @@ def rdcards( ) # save root dir from original call, pass through to _rdinclude for recursive calls try: - include_root_dir = ( - os.path.dirname(os.path.abspath(f.name)) - if include_root_dir is None - else include_root_dir + include_root_dirs = ( + ( + os.path.dirname(os.path.abspath(f.name)), + os.path.dirname(os.path.abspath(f.name)), + ) + if include_root_dirs is None + else include_root_dirs ) except AttributeError: # StringIO object, doesn't make sense to follow includes follow_includes = False - include_root_dir = None + include_root_dirs = None include_symbols = ( {symbol.lower(): path for symbol, path in include_symbols.items()} if include_symbols is not None @@ -769,7 +781,7 @@ def rdcards( "keep_comments": keep_comments, "follow_includes": follow_includes, "include_symbols": include_symbols, - "include_root_dir": include_root_dir, + "include_root_dirs": include_root_dirs, } if return_var == "dict": @@ -885,7 +897,7 @@ def _rdset(fiter, line): @guitools.read_text_file -def rdsets(f, follow_includes=True, include_symbols=None, include_root_dir=None): +def rdsets(f, follow_includes=True, include_symbols=None, include_root_dirs=None): """ Read case control SET statements from a file. @@ -904,7 +916,7 @@ def rdsets(f, follow_includes=True, include_symbols=None, include_root_dir=None) include_symbols : dict; optional A dictionary mapping Nastran symbols to an associated path. These can be read from a file using :func:`rdsymbols`. - include_root_dir : None; optional + include_root_dirs : None; optional This parameter is only used when this function is called recursively while following INCLUDE statements. Users should keep it as the default value of None. @@ -925,15 +937,18 @@ def rdsets(f, follow_includes=True, include_symbols=None, include_root_dir=None) """ # save root dir from original call, pass through to _rdinclude for recursive calls try: - include_root_dir = ( - os.path.dirname(os.path.abspath(f.name)) - if include_root_dir is None - else include_root_dir + include_root_dirs = ( + ( + os.path.dirname(os.path.abspath(f.name)), + os.path.dirname(os.path.abspath(f.name)), + ) + if include_root_dirs is None + else include_root_dirs ) except AttributeError: # StringIO object, doesn't make sense to follow includes follow_includes = False - include_root_dir = None + include_root_dirs = None include_symbols = ( {symbol.lower(): path for symbol, path in include_symbols.items()} if include_symbols is not None @@ -945,7 +960,7 @@ def rdsets(f, follow_includes=True, include_symbols=None, include_root_dir=None) kwargs = { "follow_includes": follow_includes, "include_symbols": include_symbols, - "include_root_dir": include_root_dir, + "include_root_dirs": include_root_dirs, } sets = {} diff --git a/pyyeti/tests/test_nastran.py b/pyyeti/tests/test_nastran.py index d21d6ce..d3e54c1 100644 --- a/pyyeti/tests/test_nastran.py +++ b/pyyeti/tests/test_nastran.py @@ -227,7 +227,8 @@ def check_results(cards): _wtfile(file1_path, file1.replace("file2", "../file2")) # update to use relative path # note that path is relative to file1, even though the statement is in - # file2, this is unexpected but is consistent with Nastran + # file2, this is consistent with Nastran when the INCLUDE has directories + # in addition to a filename _wtfile( os.path.join(tempdir_path, "file2.bdf"), file2.replace("file3", "../file3") ) @@ -243,6 +244,52 @@ def check_results(cards): ) check_results(cards) + # file1 in subdirectory, file2 and file3 in parent dir + with tempfile.TemporaryDirectory() as tempdir_path: + subdir_path = os.path.join(tempdir_path, "subdir") + os.mkdir(subdir_path) + file1_path = os.path.join(subdir_path, "file1.bdf") + # update to use relative path + _wtfile(file1_path, file1.replace("file2", "../file2")) + # In file2, the "INCLUDE 'file3.bdf'" statement has no directories, so file3 is + # treated as if it's in the same directory as file2. + _wtfile(os.path.join(tempdir_path, "file2.bdf"), file2) + _wtfile(os.path.join(tempdir_path, "file3.bdf"), file3) + # follow_includes is True + cards = nastran.rdcards( + file1_path, + "grid", + blank=None, + return_var="list", + keep_name=True, + follow_includes=True, + ) + check_results(cards) + + # file1 and file3 in parent_dir, file2 in subdirectory + with tempfile.TemporaryDirectory() as tempdir_path: + file1_path = os.path.join(tempdir_path, "file1.bdf") + file3_path = os.path.join(tempdir_path, "file3.bdf") + subdir_path = os.path.join(tempdir_path, "subdir") + os.mkdir(subdir_path) + file2_path = os.path.join(subdir_path, "file2.bdf") + # file2 and file3 are both included from file1 + file1_mod = file1.replace( + "INCLUDE 'file2.\nbdf'", "INCLUDE 'subdir/file2.bdf'\nINCLUDE 'file3.bdf'" + ) + _wtfile(file1_path, file1_mod) + _wtfile(file2_path, file2.replace("INCLUDE", "$ INCLUDE")) + _wtfile(file3_path, file3) + cards = nastran.rdcards( + file1_path, + "grid", + blank=None, + return_var="list", + keep_name=True, + follow_includes=True, + ) + check_results(cards) + # file1 in parent_dir, file2 and file3 in subdirectory, includes use symbols with tempfile.TemporaryDirectory() as tempdir_path: file1_path = os.path.join(tempdir_path, "file1.bdf")