Skip to content

Commit

Permalink
Merge pull request #212 from hakonhagland/maindir
Browse files Browse the repository at this point in the history
Added a more advanced method to locate the maindir from the Python scripts
  • Loading branch information
lisajulia authored Apr 16, 2024
2 parents 174f981 + a0cbccd commit 81682fd
Show file tree
Hide file tree
Showing 4 changed files with 251 additions and 3 deletions.
5 changes: 3 additions & 2 deletions scripts/python/src/fodt/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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

Expand All @@ -38,6 +38,7 @@ class Directories():
keywords = "keywords"
meta = "meta"
meta_sections = "sections"
parts = "parts"
styles = "styles"
chapters = "chapters"
sections = "sections"
Expand Down
107 changes: 107 additions & 0 deletions scripts/python/src/fodt/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,30 @@ def create_backup_document(filename) -> None:
backup_file = backupdir / filename.name
return backup_file

@staticmethod
def derive_maindir_from_filename(filename: str) -> Path:
"""
: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"
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:
Expand All @@ -40,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}"
Expand Down Expand Up @@ -75,6 +121,67 @@ 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]:
"""
: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
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
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]:
file = Helpers.keyword_file(outputdir, chapter, section)
Expand Down
16 changes: 15 additions & 1 deletion scripts/python/src/fodt/remove_span_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -256,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
Expand All @@ -268,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()

Expand Down Expand Up @@ -346,4 +351,13 @@ def remove_version_span_tags(
) -> None:
"""Remove version span tags from all .fodt subdocuments."""
logging.basicConfig(level=logging.INFO)
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()
126 changes: 126 additions & 0 deletions scripts/python/tests/test_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import os
from pathlib import Path

import pytest
from fodt.constants import Directories, FileNames
from fodt.helpers import Helpers

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
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)
)

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

0 comments on commit 81682fd

Please sign in to comment.