From a848f9a9fbafb5b8b8051ce1df462f05fa42597d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20H=C3=A6gland?= Date: Fri, 15 Mar 2024 22:32:34 +0100 Subject: [PATCH 1/2] Added a more advanced method to locate maindir Added a more advanced method to locate maindir and filename for usage with the python scripts. --- scripts/python/src/fodt/constants.py | 5 +- scripts/python/src/fodt/helpers.py | 48 +++++++++++ scripts/python/src/fodt/remove_span_tags.py | 2 + scripts/python/tests/test_helpers.py | 89 +++++++++++++++++++++ 4 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 scripts/python/tests/test_helpers.py diff --git a/scripts/python/src/fodt/constants.py b/scripts/python/src/fodt/constants.py index 4aca9340..11dee58a 100644 --- a/scripts/python/src/fodt/constants.py +++ b/scripts/python/src/fodt/constants.py @@ -9,7 +9,7 @@ class ClickOptions(): '--filename', envvar='FODT_FILENAME', required=True, - help='Name of the FODT file to extract from.' + help='Name of the FODT file to extract from. Used in combination with the --maindir option. This can be an absolute path or a relative path. If the filename is an absolute path, the --maindir option is ignored and the filename is used as is. If the filename is a relative path, and not found by concatenating maindir and filename it is searched for relative to the current working directory. If found, maindir is derived from the filename by searching its parent directories for a file main.fodt.' )(func) keyword_dir = lambda func: click.option( '--keyword-dir', @@ -27,7 +27,7 @@ def decorator(func): required=required, default=default, type=str, - help='Directory to save generated files.' + help='Directory where the main.fodt file is located. Often used in combination with the --filename option. Defaults to ../../parts (this default is based on that it is likely the user will run the script from the scripts/python directory) The environment variable FODT_MAIN_DIR can also be used to provide this value. If the filename is an absolute path, this option is ignored and maindir is derived from the filename by searching its parent directories for a file main.fodt. If the filename is a relative path, and not found by concatenating maindir and filename it is searched for relative to the current working directory. If found, maindir is derived from the filename by searching its parent directories for a file main.fodt.' )(func) return decorator @@ -38,6 +38,7 @@ class Directories(): keywords = "keywords" meta = "meta" meta_sections = "sections" + parts = "parts" styles = "styles" chapters = "chapters" sections = "sections" diff --git a/scripts/python/src/fodt/helpers.py b/scripts/python/src/fodt/helpers.py index c7b67f63..5ce5f253 100644 --- a/scripts/python/src/fodt/helpers.py +++ b/scripts/python/src/fodt/helpers.py @@ -30,6 +30,28 @@ def create_backup_document(filename) -> None: backup_file = backupdir / filename.name return backup_file + @staticmethod + def derive_maindir_from_filename(filename: str) -> Path: + """filename is a assumed to be an aboslute path to file inside maindir or + subdirectories of maindir.""" + filename = Path(filename) + assert filename.is_absolute() + # Search parent directories for main.fodt in a directory called "parts" + while True: + # Check if we have reached the root directory + # filename.parent == filename is True if filename is the root directory + if filename.parent == filename: + raise FileNotFoundError(f"Could not derive maindir from filename: " + f"Could not find '{FileNames.main_document}' in a directory " + f"called '{Directories.parts}' by searching the parent " + f"directories of filename." + ) + if filename.parent.name == Directories.parts: + if (filename.parent / FileNames.main_document).exists(): + return filename.parent + filename = filename.parent + # This should never be reached + @staticmethod def get_keyword_dir(keyword_dir: str) -> str: if keyword_dir is None: @@ -75,6 +97,32 @@ def keyword_fodt_file_path( def keywords_inverse_map(keyw_list: list[str]) -> dict[str, int]: return {keyw_list[i]: i + 1 for i in range(len(keyw_list))} + + @staticmethod + def locate_maindir_and_filename( + maindir: str, + filename: str + ) -> tuple[Path, Path]: + """filename is assumed to be a file in maindir or a file in one of its + subdirectories.""" + filename = Path(filename) + maindir = Path(maindir) # maindir can be absolute or relative + # If filename is an absolute path, ignore maindir + if filename.is_absolute(): + assert filename.exists() + maindir = Helpers.derive_maindir_from_filename(filename) + return maindir, Path(filename) + else: + # Try to find filename by concatenating maindir and filename + filename = maindir / filename + if filename.exists(): + return maindir, filename + # If not found, search for filename relative to the current working directory + filename = Path.cwd() / filename + if filename.exists(): + maindir = Helpers.derive_maindir_from_filename(filename) + return maindir, filename + @staticmethod def read_keyword_order(outputdir: Path, chapter: int, section: int) -> list[str]: file = Helpers.keyword_file(outputdir, chapter, section) diff --git a/scripts/python/src/fodt/remove_span_tags.py b/scripts/python/src/fodt/remove_span_tags.py index 59485ae0..46bf33ba 100644 --- a/scripts/python/src/fodt/remove_span_tags.py +++ b/scripts/python/src/fodt/remove_span_tags.py @@ -9,6 +9,7 @@ import click from fodt.constants import ClickOptions +from fodt.helpers import Helpers from fodt.xml_helpers import XMLHelper class RemoveEmptyLinesHandler(xml.sax.handler.ContentHandler): @@ -346,4 +347,5 @@ def remove_version_span_tags( ) -> None: """Remove version span tags from all .fodt subdocuments.""" logging.basicConfig(level=logging.INFO) + maindir, filename = Helpers.locate_maindir_and_filename(maindir, filename) RemoveSpanTags(maindir, filename, max_files).remove_span_tags() diff --git a/scripts/python/tests/test_helpers.py b/scripts/python/tests/test_helpers.py new file mode 100644 index 00000000..88da1c5c --- /dev/null +++ b/scripts/python/tests/test_helpers.py @@ -0,0 +1,89 @@ +import os +from pathlib import Path + +import pytest +from fodt.constants import Directories, FileNames +from fodt.helpers import Helpers + +class TestLocateMainDir: + def test_locate_with_absolute_path_exists(self, tmp_path: Path) -> None: + """Test locating maindir and filename when the maindir is given as an absolute path.""" + maindir = tmp_path / Directories.parts + maindir.mkdir() + mainfile = maindir / FileNames.main_document + mainfile.touch() + filename_dir = maindir / Directories.chapters + filename_dir.mkdir() + filename = filename_dir / "1.fodt" + filename.touch() + result_maindir, result_filename = Helpers.locate_maindir_and_filename( + str(maindir), str(filename) + ) + assert result_maindir == maindir + assert result_filename == filename + + def test_locate_with_absolute_path_exists_no_main(self, tmp_path: Path) -> None: + maindir = tmp_path / Directories.parts + maindir.mkdir() + mainfile = maindir / FileNames.main_document + # mainfile.touch() # Do not create the main file + filename_dir = maindir / Directories.chapters + filename_dir.mkdir() + filename = filename_dir / "1.fodt" + filename.touch() + with pytest.raises(FileNotFoundError) as excinfo: + Helpers.locate_maindir_and_filename( + str(maindir), str(filename) + ) + assert (f"Could not find '{FileNames.main_document}' in a directory " + f"called '{Directories.parts}'" in str(excinfo.value)) + + def test_locate_with_relative_path_in_maindir_exists(self, tmp_path: Path) -> None: + maindir = tmp_path / Directories.parts + maindir.mkdir() + mainfile = maindir / FileNames.main_document + mainfile.touch() # Ensure the main document exists + # Change directory to maindir + os.chdir(str(maindir)) + filename_dir = Path(Directories.appendices) + filename_dir.mkdir() + filename = "A.fodt" + filename_path = filename_dir / filename + filename_path.touch() # Create the file within maindir + filename_abs_path = maindir / filename_path + result_maindir, result_filename = Helpers.locate_maindir_and_filename( + str(maindir), str(filename_path) + ) + assert result_maindir == maindir + assert result_filename == filename_abs_path + + def test_locate_with_relative_path_not_in_maindir_but_in_cwd( + self, tmp_path: Path + ): + cwd = tmp_path / "cwd" + cwd.mkdir() + os.chdir(str(cwd)) + filename = "1.fodt" + filename_path = cwd / filename + filename_path.touch() # Create the file in CWD + maindir = tmp_path # Some dummy path that is not the maindir + with pytest.raises(FileNotFoundError) as excinfo: + Helpers.locate_maindir_and_filename( + str(maindir), str(filename_path) + ) + assert excinfo.match( + f"Could not find '{FileNames.main_document}' in a directory " + f"called '{Directories.parts}' by searching the parent " + f"directories of filename." + ) + + def test_locate_with_absolute_path_not_exists(self, tmp_path: Path): + maindir = tmp_path / Directories.parts + maindir.mkdir() + filename = tmp_path / "nonexistent.fodt" + # Do not create the file, simulating a non-existent file scenario + + with pytest.raises(AssertionError): + Helpers.locate_maindir_and_filename( + str(maindir), str(filename) + ) From a0cbccd0adace56b8f6d8050937d3e0035419811 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20H=C3=A6gland?= Date: Tue, 16 Apr 2024 11:05:15 +0200 Subject: [PATCH 2/2] Improved location of maindir Improved the algorithm for location of maindir when filename is not given. Fixed remove-span-tags script to not assume that filename is relative to maindir --- scripts/python/src/fodt/helpers.py | 73 +++++++++++++++++++-- scripts/python/src/fodt/remove_span_tags.py | 16 ++++- scripts/python/tests/test_helpers.py | 39 ++++++++++- 3 files changed, 118 insertions(+), 10 deletions(-) diff --git a/scripts/python/src/fodt/helpers.py b/scripts/python/src/fodt/helpers.py index 5ce5f253..5e903140 100644 --- a/scripts/python/src/fodt/helpers.py +++ b/scripts/python/src/fodt/helpers.py @@ -32,8 +32,10 @@ def create_backup_document(filename) -> None: @staticmethod def derive_maindir_from_filename(filename: str) -> Path: - """filename is a assumed to be an aboslute path to file inside maindir or - subdirectories of maindir.""" + """ + :param filename: Assumed to be an aboslute path to file inside maindir or subdirectories of maindir. + :return: The absolute path to the maindir. + """ filename = Path(filename) assert filename.is_absolute() # Search parent directories for main.fodt in a directory called "parts" @@ -62,6 +64,28 @@ def get_keyword_dir(keyword_dir: str) -> str: raise FileNotFoundError(f"Keyword names directory not found.") return keyword_dir + @staticmethod + def get_maindir(maindir: str) -> Path: + """ + :param maindir: The main directory of the project. Can be relative or absolute. + :return: The absolute path to the main directory. + """ + if maindir is None: + # Try to find maindir by searching the current working directory and its + # parent directories for a file main.fodt inside a directory called parts + maindir = Helpers.locate_maindir_from_current_dir() + else: + maindir = Path(maindir) + if not maindir.is_dir(): + # The default value for maindir is a relative path like "../../parts" + # If it does not exist, try to find maindir by searching the current + # working directory and its parent directories. This is better than + # raising an exception here I think.. + maindir = Helpers.locate_maindir_from_current_dir() + else: + maindir = maindir.absolute() + return maindir + @staticmethod def keyword_file(outputdir: Path, chapter: int, section: int) -> Path: directory = f"{chapter}.{section}" @@ -103,8 +127,10 @@ def locate_maindir_and_filename( maindir: str, filename: str ) -> tuple[Path, Path]: - """filename is assumed to be a file in maindir or a file in one of its - subdirectories.""" + """ + :param maindir: The main directory of the project. Can be relative or absolute. + :param filename: The filename to locate. Can be relative or absolute. ``filename`` is assumed to be a file in maindir or a file in one of its subdirectories. + :return: A tuple of the form (maindir, filename), where both are absolute paths.""" filename = Path(filename) maindir = Path(maindir) # maindir can be absolute or relative # If filename is an absolute path, ignore maindir @@ -114,14 +140,47 @@ def locate_maindir_and_filename( return maindir, Path(filename) else: # Try to find filename by concatenating maindir and filename - filename = maindir / filename - if filename.exists(): - return maindir, filename + if not maindir.is_absolute(): + # If both maindir and filename are relative, make filename relative + # to maindir instead of relative to the current working directory + maindir_abs = Path.cwd() / maindir + filename_abs = maindir_abs / filename + if filename_abs.exists(): + return maindir_abs, filename_abs + else: + filename = maindir / filename + if filename.exists(): + return maindir, filename # If not found, search for filename relative to the current working directory filename = Path.cwd() / filename if filename.exists(): maindir = Helpers.derive_maindir_from_filename(filename) return maindir, filename + raise FileNotFoundError(f"Could not find '{filename.name}' in a directory " + f"called '{maindir.name}'.") + + + @staticmethod + def locate_maindir_from_current_dir() -> Path: + cwd = Path.cwd() + # We cannot use derive_maindir_from_filename() here because cwd does not + # have to be inside maindir in this case + while True: + # Check if we have reached the root directory + # cwd.parent == cwd is True if filename is the root directory + if cwd.parent == cwd: + raise FileNotFoundError(f"Could not derive maindir from cwd: " + f"Could not find '{FileNames.main_document}' in a directory " + f"called '{Directories.parts}' by searching the parent " + f"directories of cwd." + ) + # Check if there is a sibling directory called "parts" with a file main.fodt + dir_ = cwd / Directories.parts + if dir_.is_dir(): + if (dir_ / FileNames.main_document).exists(): + return dir_ + cwd = cwd.parent + # This line should never be reached @staticmethod def read_keyword_order(outputdir: Path, chapter: int, section: int) -> list[str]: diff --git a/scripts/python/src/fodt/remove_span_tags.py b/scripts/python/src/fodt/remove_span_tags.py index 46bf33ba..fbc55d59 100644 --- a/scripts/python/src/fodt/remove_span_tags.py +++ b/scripts/python/src/fodt/remove_span_tags.py @@ -257,6 +257,9 @@ def __init__(self, maindir: str, filename: str|None, max_files: int|None) -> Non self.maindir = Path(maindir) self.filename = filename self.max_files = max_files + assert self.maindir.is_absolute() + assert self.filename is None or Path(self.filename).is_absolute() + assert self.maindir.is_dir() def remove_empty_lines(self, filename: Path) -> None: # Remove empty lines from the automtic-styles section @@ -269,7 +272,8 @@ def remove_empty_lines(self, filename: Path) -> None: def remove_span_tags(self) -> None: if self.filename: - self.remove_span_tags_and_styles_from_file(self.maindir / self.filename) + # NOTE: self.filename is an absolute path + self.remove_span_tags_and_styles_from_file(self.filename) else: self.remove_span_tags_from_all_files() @@ -347,5 +351,13 @@ def remove_version_span_tags( ) -> None: """Remove version span tags from all .fodt subdocuments.""" logging.basicConfig(level=logging.INFO) - maindir, filename = Helpers.locate_maindir_and_filename(maindir, filename) + if filename is not None: + filename = Path(filename) + assert filename.is_absolute() + maindir, filename = Helpers.locate_maindir_and_filename(maindir, filename) + else: + # Convert maindir to an absolute path + maindir = Helpers.get_maindir(maindir) + maindir = Path(maindir).absolute() + assert maindir.is_dir() RemoveSpanTags(maindir, filename, max_files).remove_span_tags() diff --git a/scripts/python/tests/test_helpers.py b/scripts/python/tests/test_helpers.py index 88da1c5c..0109a3a0 100644 --- a/scripts/python/tests/test_helpers.py +++ b/scripts/python/tests/test_helpers.py @@ -5,7 +5,7 @@ from fodt.constants import Directories, FileNames from fodt.helpers import Helpers -class TestLocateMainDir: +class TestLocateMainDirAndFilename: def test_locate_with_absolute_path_exists(self, tmp_path: Path) -> None: """Test locating maindir and filename when the maindir is given as an absolute path.""" maindir = tmp_path / Directories.parts @@ -87,3 +87,40 @@ def test_locate_with_absolute_path_not_exists(self, tmp_path: Path): Helpers.locate_maindir_and_filename( str(maindir), str(filename) ) + +class TestLocateMainDirFromCwd: + def test_locate_exists_in_cwd(self, tmp_path: Path): + """Test locating maindir from the current working directory when the maindir + exists in the current working directory.""" + maindir = tmp_path / Directories.parts + maindir.mkdir() + mainfile = maindir / FileNames.main_document + mainfile.touch() + os.chdir(str(tmp_path)) + result = Helpers.locate_maindir_from_current_dir() + assert result == maindir + + def test_locate_exists_as_parent(self, tmp_path: Path): + """Test locating maindir from the current working directory when the maindir + is the parent of the current working directory.""" + maindir = tmp_path / Directories.parts + maindir.mkdir() + mainfile = maindir / FileNames.main_document + mainfile.touch() + os.chdir(str(maindir)) + result = Helpers.locate_maindir_from_current_dir() + assert result == maindir + + def test_locate_exists_as_sibling_of_parent(self, tmp_path: Path): + """Test locating maindir from the current working directory when the maindir + is a sibling of the parent of the current working directory.""" + maindir = tmp_path / Directories.parts + maindir.mkdir() + mainfile = maindir / FileNames.main_document + mainfile.touch() + os.chdir(str(tmp_path)) + subdir = tmp_path / "subdir" + subdir.mkdir() + os.chdir(str(subdir)) + result = Helpers.locate_maindir_from_current_dir() + assert result == maindir