diff --git a/opensiddur/exporter/compiler.py b/opensiddur/exporter/compiler.py index f602ea2..484717e 100644 --- a/opensiddur/exporter/compiler.py +++ b/opensiddur/exporter/compiler.py @@ -25,10 +25,11 @@ from lxml import etree from opensiddur.exporter.constants import JLPTEI_NAMESPACE, PROCESSING_NAMESPACE -from opensiddur.exporter.linear import LinearData, get_linear_data +from opensiddur.exporter.linear import LinearData, get_linear_data, reset_linear_data from opensiddur.exporter.refdb import ReferenceDatabase from opensiddur.exporter.settings import load_default_settings, load_settings from opensiddur.exporter.urn import ResolvedUrnRange, UrnResolver +from opensiddur.common.constants import PROJECT_DIRECTORY class _ProcessingCommand(Enum): """ Possible ways the compiler can process an element """ @@ -599,18 +600,37 @@ def process(self, root: Optional[ElementBase] = None): return copied -def main(): # pragma: no cover +def main(argv: list[str] | None = None): # pragma: no cover parser = argparse.ArgumentParser(description="Compile a TEI file with external references to a single file.") parser.add_argument("--project", "-p", type=str, help="The project name.", required=True) parser.add_argument("--file_name", "-f", type=str, help="The file name (relative to the project).", required=True) parser.add_argument("--output_file", "-o", type=str, help="The output XML file.") parser.add_argument("--settings", "-s", type=Path, help="YAML file with compiler settings. See README.md for more details.") - args = parser.parse_args() + parser.add_argument( + "--project-directory", + type=Path, + default=PROJECT_DIRECTORY, + help="Base directory containing project subdirectories (default: /project).", + ) + args = parser.parse_args(argv) + + project_directory = args.project_directory.resolve() + reset_linear_data() + linear_data = get_linear_data() if args.settings: - linear_data = load_settings(args.settings) + linear_data = load_settings( + args.settings, + linear_data=linear_data, + project_directory=project_directory, + ) else: - linear_data = load_default_settings(args.project, args.file_name) + linear_data = load_default_settings( + args.project, + args.file_name, + linear_data=linear_data, + project_directory=project_directory, + ) from opensiddur.exporter.external_compiler import ExternalCompilerProcessor compiler = ExternalCompilerProcessor(args.project, args.file_name, linear_data=linear_data) diff --git a/opensiddur/exporter/pdf/pdf.py b/opensiddur/exporter/pdf/pdf.py index 3d3a143..df1801d 100755 --- a/opensiddur/exporter/pdf/pdf.py +++ b/opensiddur/exporter/pdf/pdf.py @@ -31,12 +31,14 @@ sys.path.insert(0, str(project_root)) from opensiddur.exporter.tex.latex import transform_xml_to_tex # noqa: E402 +from opensiddur.common.constants import PROJECT_DIRECTORY # noqa: E402 def generate_tex( input_file: Path, temp_tex_file: Path, settings_file: Optional[Path] = None, + project_directory: Path = PROJECT_DIRECTORY, ) -> bool: """Generate a LuaLaTeX file from compiled JLPTEI XML. @@ -55,6 +57,7 @@ def generate_tex( str(input_file), output_file=str(temp_tex_file), settings_file=settings_file, + project_directory=project_directory, ) print(f"TeX file generated: {temp_tex_file}", file=sys.stderr) return True @@ -275,6 +278,7 @@ def export_to_pdf( settings_file: Optional[Path] = None, tex_output: Optional[Path] = None, build_dir: Optional[Path] = None, + project_directory: Path = PROJECT_DIRECTORY, ) -> bool: """Convert a compiled JLPTEI XML file to PDF. @@ -296,7 +300,12 @@ def export_to_pdf( if tex_output is not None: tex_output.parent.mkdir(parents=True, exist_ok=True) - if not generate_tex(input_file, temp_tex_file, settings_file=settings_file): + if not generate_tex( + input_file, + temp_tex_file, + settings_file=settings_file, + project_directory=project_directory, + ): return False if not compile_tex_to_pdf(temp_tex_file, output_pdf, build_dir=build_dir): @@ -353,6 +362,12 @@ def main(): # pragma: no cover default=None, help="Directory to keep LaTeX build artifacts (.log, .aux, etc.) for debugging.", ) + parser.add_argument( + "--project-directory", + type=Path, + default=PROJECT_DIRECTORY, + help="Base directory containing project subdirectories (default: /project).", + ) args = parser.parse_args() @@ -372,6 +387,7 @@ def main(): # pragma: no cover settings_file=args.settings_file, tex_output=tex_output, build_dir=args.build_dir, + project_directory=args.project_directory, ): sys.exit(1) diff --git a/opensiddur/exporter/refdb.py b/opensiddur/exporter/refdb.py index f0a4d35..25bf9f9 100644 --- a/opensiddur/exporter/refdb.py +++ b/opensiddur/exporter/refdb.py @@ -1,5 +1,6 @@ """ Reference Database """ +import argparse from pathlib import Path import re import sqlite3 @@ -679,18 +680,37 @@ def __exit__(self, exc_type, exc_val, exc_tb): self.close() -def main(): # pragma: no cover +def main(argv: list[str] | None = None) -> None: # pragma: no cover """Synchronize the reference database with the project directory. - + Opens the default database and syncs all projects, printing a summary of the changes made. """ - print(f"Synchronizing reference database: {INDEX_DB_FILE}") - print(f"Project directory: {PROJECT_DIRECTORY}\n") - - with ReferenceDatabase(INDEX_DB_FILE) as refdb: + parser = argparse.ArgumentParser( + description="Synchronize the reference database with JLPTEI project files." + ) + parser.add_argument( + "--project-directory", + type=Path, + default=PROJECT_DIRECTORY, + help="Base directory containing project subdirectories (default: /project).", + ) + parser.add_argument( + "--reference-db", + type=Path, + default=INDEX_DB_FILE, + help="Path to reference.db (default: /database/reference.db).", + ) + args = parser.parse_args(argv) + project_directory = args.project_directory.resolve() + reference_db_path = args.reference_db.resolve() + + print(f"Synchronizing reference database: {reference_db_path}") + print(f"Project directory: {project_directory}\n") + + with ReferenceDatabase(reference_db_path) as refdb: try: - result = refdb.sync_projects(PROJECT_DIRECTORY) + result = refdb.sync_projects(project_directory) # Print summary print("=" * 70) diff --git a/opensiddur/exporter/settings.py b/opensiddur/exporter/settings.py index d9a3b9f..3f1bc97 100644 --- a/opensiddur/exporter/settings.py +++ b/opensiddur/exporter/settings.py @@ -3,15 +3,23 @@ from enum import StrEnum from pathlib import Path from typing import Optional -from pydantic import BaseModel, Field, field_validator +from pydantic import BaseModel, Field, ValidationInfo, field_validator import yaml from opensiddur.exporter.linear import LinearData, ParallelColumnOrder, get_linear_data from opensiddur.common.constants import PROJECT_DIRECTORY -def _validate_project_list(project_list: list[str], - project_directory: Optional[Path] = None) -> list[str]: - """ Validate a list of projects. """ + +def _project_directory_from_context(info: ValidationInfo) -> Path: + context = info.context or {} + return Path(context.get("project_directory") or PROJECT_DIRECTORY) + + +def _validate_project_list( + project_list: list[str], + project_directory: Optional[Path] = None, +) -> list[str]: + """Validate a list of projects.""" if project_directory is None: project_directory = PROJECT_DIRECTORY # looked up at call time so tests can patch it for project in project_list: @@ -19,25 +27,36 @@ def _validate_project_list(project_list: list[str], raise ValueError(f"Project {project} does not exist") return project_list + +def _apply_project_directory( + linear_data: LinearData, + project_directory: Path, +) -> None: + linear_data.xml_cache.base_path = project_directory.resolve() + + class Prioritizations(BaseModel): transclusion: list[str] = Field(default_factory=list) instructions: list[str] = Field(default_factory=list) @field_validator("transclusion") - def validate_transclusion(cls, v: list[str]) -> list[str]: - return _validate_project_list(v) + @classmethod + def validate_transclusion(cls, v: list[str], info: ValidationInfo) -> list[str]: + return _validate_project_list(v, _project_directory_from_context(info)) @field_validator("instructions") - def validate_instructions(cls, v: list[str]) -> list[str]: - return _validate_project_list(v) + @classmethod + def validate_instructions(cls, v: list[str], info: ValidationInfo) -> list[str]: + return _validate_project_list(v, _project_directory_from_context(info)) class ParallelConfig(BaseModel): projects: list[str] = Field(default_factory=list) column_order: ParallelColumnOrder = ParallelColumnOrder.PRIMARY_FIRST @field_validator("projects") - def validate_projects(cls, v: list[str]) -> list[str]: - return _validate_project_list(v) + @classmethod + def validate_projects(cls, v: list[str], info: ValidationInfo) -> list[str]: + return _validate_project_list(v, _project_directory_from_context(info)) class ParallelLayout(StrEnum): @@ -85,16 +104,26 @@ class SettingsYaml(BaseModel): typography: TypographyConfig = Field(default_factory=TypographyConfig) @field_validator("annotations") - def validate_annotations(cls, v: list[str]) -> list[str]: - return _validate_project_list(v) - -def load_settings(settings_file: Path, linear_data: Optional[LinearData] = None) -> LinearData: + @classmethod + def validate_annotations(cls, v: list[str], info: ValidationInfo) -> list[str]: + return _validate_project_list(v, _project_directory_from_context(info)) + +def load_settings( + settings_file: Path, + linear_data: Optional[LinearData] = None, + project_directory: Optional[Path] = None, +) -> LinearData: """ Load settings into linear data from a YAML file. """ + project_directory = Path(project_directory or PROJECT_DIRECTORY).resolve() with open(settings_file, 'r') as f: data = yaml.safe_load(f) - settings = SettingsYaml.model_validate(data) + settings = SettingsYaml.model_validate( + data, + context={"project_directory": project_directory}, + ) linear_data = linear_data or get_linear_data() + _apply_project_directory(linear_data, project_directory) linear_data.project_priority = settings.priority.transclusion linear_data.instruction_priority = settings.priority.instructions linear_data.annotation_projects = settings.annotations @@ -107,9 +136,13 @@ def load_settings(settings_file: Path, linear_data: Optional[LinearData] = None) def load_default_settings( project: str, file_name: str, - linear_data: Optional[LinearData] = None) -> LinearData: + linear_data: Optional[LinearData] = None, + project_directory: Optional[Path] = None, +) -> LinearData: """ Load default settings into linear data. """ + project_directory = Path(project_directory or PROJECT_DIRECTORY).resolve() linear_data = linear_data or get_linear_data() + _apply_project_directory(linear_data, project_directory) linear_data.project_priority = [project] linear_data.instruction_priority = [project] linear_data.annotation_projects = [project] diff --git a/opensiddur/exporter/tex/latex.py b/opensiddur/exporter/tex/latex.py index 649f585..6b7e4a8 100644 --- a/opensiddur/exporter/tex/latex.py +++ b/opensiddur/exporter/tex/latex.py @@ -24,14 +24,17 @@ # Add the project root to the Python path project_root = Path(__file__).resolve().parent.parent.parent.parent -projects_source_root = project_root / "project" sys.path.insert(0, str(project_root)) from opensiddur.common.xslt import xslt_transform_string # noqa: E402 +from opensiddur.common.constants import PROJECT_DIRECTORY # noqa: E402 from opensiddur.exporter.settings import TypographyConfig # noqa: E402 XSLT_FILE = Path(__file__).parent / "reledmac.xslt" +# Default project root for resolving p:project/p:file_name references in compiled XML. +projects_source_root = PROJECT_DIRECTORY + class LicenseRecord(BaseModel): """Record of the license for a given file.""" @@ -49,8 +52,14 @@ class CreditRecord(BaseModel): contributor: str # contributor name at the source -def extract_licenses(xml_file_paths: list[Path]) -> dict[Path, LicenseRecord]: +def extract_licenses( + xml_file_paths: list[Path], + project_directory: Path | None = None, +) -> dict[Path, LicenseRecord]: """Extract license URLs and names from a list of JLPTEI XML files.""" + if project_directory is None: + project_directory = projects_source_root + project_directory = project_directory.resolve() ns = {"tei": "http://www.tei-c.org/ns/1.0"} results: dict[Path, LicenseRecord] = {} @@ -58,10 +67,10 @@ def extract_licenses(xml_file_paths: list[Path]) -> dict[Path, LicenseRecord]: for file_path in xml_file_paths: try: try: - relative_path = file_path.absolute().relative_to(projects_source_root) + relative_path = file_path.absolute().relative_to(project_directory) except ValueError: print( - f"Warning: {file_path} is not a subdirectory of {projects_source_root}", + f"Warning: {file_path} is not a subdirectory of {project_directory}", file=sys.stderr, ) continue @@ -253,13 +262,16 @@ def extract_sources(xml_file_paths: list[Path]) -> tuple[str, str]: def get_file_references( - input_file: Path, project_directory: Path = projects_source_root + input_file: Path, project_directory: Path | None = None ) -> list[Path]: """Get all source file references from a compiled JLPTEI XML file. Includes the file itself, all transcluded files, and the ``index.xml`` of every referenced project. """ + if project_directory is None: + project_directory = projects_source_root + project_directory = project_directory.resolve() ns = { "tei": "http://www.tei-c.org/ns/1.0", "p": "http://jewishliturgy.org/ns/processing", @@ -319,6 +331,7 @@ def transform_xml_to_tex( output_file: Optional[str] = None, settings_file: Optional[Path] = None, typography: Optional[TypographyConfig] = None, + project_directory: Path | None = None, ) -> str: """Transform a compiled JLPTEI XML file into a LuaLaTeX document. @@ -328,6 +341,7 @@ def transform_xml_to_tex( output_file: If given, write to this path; otherwise return the string. settings_file: Optional path to a settings.yaml to read typography from. typography: Pre-loaded TypographyConfig (takes precedence over settings_file). + project_directory: Base directory containing project subdirectories. Returns: The transformed LaTeX content as a string. @@ -336,9 +350,12 @@ def transform_xml_to_tex( with open(input_file, "r", encoding="utf-8") as input_fd: input_xml = input_fd.read() - file_references = get_file_references(input_file, projects_source_root) + if project_directory is None: + project_directory = projects_source_root + project_directory = project_directory.resolve() + file_references = get_file_references(input_file, project_directory) - licenses = extract_licenses(file_references) + licenses = extract_licenses(file_references, project_directory) licenses_tex = licenses_to_tex(group_licenses(licenses)) credits = extract_credits(file_references) credits_tex = credits_to_tex(group_credits(credits)) @@ -421,6 +438,12 @@ def main(): # pragma: no cover default=str(XSLT_FILE), help="Path to the XSLT file (default: reledmac.xslt next to this script)", ) + parser.add_argument( + "--project-directory", + type=Path, + default=PROJECT_DIRECTORY, + help="Base directory containing project subdirectories (default: /project).", + ) args = parser.parse_args() @@ -437,6 +460,7 @@ def main(): # pragma: no cover xslt_file=Path(args.xslt_file), output_file=args.output_file, settings_file=args.settings_file, + project_directory=args.project_directory, ) diff --git a/opensiddur/importer/jps1917/convert_wikisource.py b/opensiddur/importer/jps1917/convert_wikisource.py index b87aa5d..30e755c 100644 --- a/opensiddur/importer/jps1917/convert_wikisource.py +++ b/opensiddur/importer/jps1917/convert_wikisource.py @@ -18,6 +18,25 @@ MEDIAWIKI_TO_TEI_XSLT = Path(__file__).parent / "mediawiki_to_tei.xslt" + +def _repo_root() -> Path: + return Path(__file__).resolve().parent.parent.parent.parent + + +def make_project_directory(project_dir: Path | None = None) -> Path: + """Create the JPS 1917 project directory if missing; return its path.""" + directory = ( + project_dir.resolve() + if project_dir is not None + else PROJECT_DIRECTORY / "jps1917" + ) + directory.mkdir(parents=True, exist_ok=True) + return directory + + +def _default_project_directory() -> Path: + return PROJECT_DIRECTORY / "jps1917" + class Book(BaseModel): book_name_he: str book_name_en: str @@ -509,8 +528,15 @@ def process_mediawiki( # f.write(pre_xml) return mediawiki_xml_to_tei(pre_xml, xslt_params=kwargs) -def validate_and_write_tei_file(tei_content: str, file_name: str): - out_path = PROJECT_DIRECTORY / "jps1917" / f"{file_name}.xml" +def validate_and_write_tei_file( + tei_content: str, + file_name: str, + project_dir: Path | None = None, +): + directory = ( + project_dir.resolve() if project_dir is not None else _default_project_directory() + ) + out_path = directory / f"{file_name}.xml" print(f"Writing {out_path}") pretty_xml = prettify_xml(tei_content, remove_xml_declaration=True) is_valid, errors = validate(pretty_xml) @@ -519,7 +545,11 @@ def validate_and_write_tei_file(tei_content: str, file_name: str): with open(out_path, "w") as f: f.write(pretty_xml) -def book_file(book: Book, sourcetexts_root: Path | None = None) -> str: +def book_file( + book: Book, + sourcetexts_root: Path | None = None, + project_dir: Path | None = None, +) -> str: transcription_credits = get_credits_pages( book.start_page, book.end_page, sourcetexts_root ) @@ -544,12 +574,16 @@ def book_file(book: Book, sourcetexts_root: Path | None = None) -> str: ) with open("temp.tei.xml", "w") as f: f.write(tei_content) - validate_and_write_tei_file(tei_content, book.file_name) + validate_and_write_tei_file(tei_content, book.file_name, project_dir) return tei_content -def index_file(idx: Index, sourcetexts_root: Path | None = None) -> str: +def index_file( + idx: Index, + sourcetexts_root: Path | None = None, + project_dir: Path | None = None, +) -> str: if idx.start_page is not None and idx.end_page is not None: transcription_credits = get_credits_pages( idx.start_page, idx.end_page, sourcetexts_root @@ -594,21 +628,28 @@ def index_file(idx: Index, sourcetexts_root: Path | None = None) -> str: ) with open("temp.tei.xml", "w") as f: f.write(tei_content) - validate_and_write_tei_file(tei_content, idx.file_name) + validate_and_write_tei_file(tei_content, idx.file_name, project_dir) for transclusion in idx.transclusions: if isinstance(transclusion, Index): - index_file(transclusion, sourcetexts_root) + index_file(transclusion, sourcetexts_root, project_dir) else: - book_file(transclusion, sourcetexts_root) + book_file(transclusion, sourcetexts_root, project_dir) return tei_content -def main(argv: list[str] | None = None) -> None: # pragma: no cover +def _build_arg_parser() -> argparse.ArgumentParser: + repo = _repo_root() parser = argparse.ArgumentParser( description="Convert JPS 1917 Wikisource page dumps to JLPTEI under project/jps1917." ) + parser.add_argument( + "--project-dir", + type=Path, + default=repo / "project" / "jps1917", + help="Output directory for generated JLPTEI (default: /project/jps1917).", + ) parser.add_argument( "--sourcetexts-root", type=Path, @@ -618,10 +659,14 @@ def main(argv: list[str] | None = None) -> None: # pragma: no cover "/jps1917 (default: /sources)." ), ) - args = parser.parse_args(argv) - (PROJECT_DIRECTORY / "jps1917").mkdir(parents=True, exist_ok=True) + return parser + + +def main(argv: list[str] | None = None) -> None: # pragma: no cover + args = _build_arg_parser().parse_args(argv) + project_directory = make_project_directory(args.project_dir) for part in JPS_1917: - index_file(part, args.sourcetexts_root) + index_file(part, args.sourcetexts_root, project_directory) if __name__ == "__main__": # pragma: no cover diff --git a/opensiddur/tests/exporter/test_compiler.py b/opensiddur/tests/exporter/test_compiler.py index 7789e42..87e0885 100644 --- a/opensiddur/tests/exporter/test_compiler.py +++ b/opensiddur/tests/exporter/test_compiler.py @@ -1958,3 +1958,42 @@ def _mark_file_source(self, element): self.assertNotIn(f"{{{self.TEI_NS}}}TEI", children_tags) +class TestCompilerMain(unittest.TestCase): + """Tests for compiler CLI main().""" + + @patch("opensiddur.exporter.external_compiler.ExternalCompilerProcessor") + @patch("opensiddur.exporter.compiler.load_default_settings") + @patch("opensiddur.exporter.compiler.reset_linear_data") + def test_main_uses_project_directory( + self, + mock_reset_linear_data, + mock_load_default_settings, + mock_processor_class, + ): + from opensiddur.exporter.compiler import main as compiler_main + + mock_linear_data = MagicMock() + mock_load_default_settings.return_value = mock_linear_data + mock_processor = MagicMock() + mock_processor.process.return_value = [etree.Element("root")] + mock_processor_class.return_value = mock_processor + + project_dir = Path("/custom/project") + with tempfile.TemporaryDirectory() as td: + output_file = Path(td) / "out.xml" + compiler_main([ + "--project", "wlc", + "--file_name", "genesis.xml", + "--output_file", str(output_file), + "--project-directory", str(project_dir), + ]) + + mock_load_default_settings.assert_called_once() + _, kwargs = mock_load_default_settings.call_args + self.assertEqual(kwargs["project_directory"], project_dir.resolve()) + mock_processor_class.assert_called_once_with( + "wlc", + "genesis.xml", + linear_data=mock_linear_data, + ) + diff --git a/opensiddur/tests/exporter/test_parallel_settings.py b/opensiddur/tests/exporter/test_parallel_settings.py index 0138d5c..8ce31d3 100644 --- a/opensiddur/tests/exporter/test_parallel_settings.py +++ b/opensiddur/tests/exporter/test_parallel_settings.py @@ -30,11 +30,8 @@ def _write_yaml(self, data: dict) -> Path: return path def _settings_with_patch(self, data: dict) -> LinearData: - from unittest.mock import patch - from opensiddur.common.constants import PROJECT_DIRECTORY path = self._write_yaml(data) - with patch("opensiddur.exporter.settings.PROJECT_DIRECTORY", self.project_dir): - return load_settings(path) + return load_settings(path, project_directory=self.project_dir) # ── Basic loading ─────────────────────────────────────────────────────── @@ -80,11 +77,14 @@ def test_invalid_parallel_project_raises(self): # ── Default settings ──────────────────────────────────────────────────── def test_load_default_settings_no_parallel(self): - from unittest.mock import patch - with patch("opensiddur.exporter.settings.PROJECT_DIRECTORY", self.project_dir): - ld = load_default_settings("proj-a", "index.xml") + ld = load_default_settings( + "proj-a", + "index.xml", + project_directory=self.project_dir, + ) self.assertEqual(ld.parallel_projects, []) self.assertEqual(ld.parallel_column_order, ParallelColumnOrder.PRIMARY_FIRST) + self.assertEqual(ld.xml_cache.base_path, self.project_dir.resolve()) if __name__ == "__main__": diff --git a/opensiddur/tests/exporter/test_pdf.py b/opensiddur/tests/exporter/test_pdf.py index fbc6201..c48bd54 100644 --- a/opensiddur/tests/exporter/test_pdf.py +++ b/opensiddur/tests/exporter/test_pdf.py @@ -4,7 +4,7 @@ import tempfile import unittest from pathlib import Path -from unittest.mock import MagicMock, Mock, patch +from unittest.mock import MagicMock, Mock, patch, ANY from opensiddur.exporter.pdf.pdf import ( _have_command, @@ -49,6 +49,7 @@ def write_tex(path, output_file=None, **kwargs): str(input_file), output_file=str(output_file), settings_file=None, + project_directory=ANY, ) def test_generate_tex_forwards_settings_file(self): @@ -65,6 +66,7 @@ def test_generate_tex_forwards_settings_file(self): str(input_file), output_file=str(output_file), settings_file=settings_file, + project_directory=ANY, ) def test_generate_tex_handles_exception(self): diff --git a/opensiddur/tests/importer/jps1917/test_convert_wikisource.py b/opensiddur/tests/importer/jps1917/test_convert_wikisource.py index 7b93b62..7c618c5 100644 --- a/opensiddur/tests/importer/jps1917/test_convert_wikisource.py +++ b/opensiddur/tests/importer/jps1917/test_convert_wikisource.py @@ -6,7 +6,7 @@ from opensiddur.importer.jps1917.convert_wikisource import ( get_credits_pages, header, tei_file, process_mediawiki, validate_and_write_tei_file, - book_file, index_file, Book, Index, mediawiki_xml_to_tei + book_file, index_file, Book, Index, mediawiki_xml_to_tei, main, make_project_directory, ) from opensiddur.importer.util.validation import validate_with_start, validate @@ -1164,7 +1164,7 @@ def test_book_file_basic_flow(self, mock_file, mock_get_credits, mock_header, mock_file().write.assert_called_once_with("Complete TEI content") # Verify validate_and_write_tei_file was called - mock_validate_write.assert_called_once_with("Complete TEI content", "genesis") + mock_validate_write.assert_called_once_with("Complete TEI content", "genesis", None) # Verify return value self.assertEqual(result, "Complete TEI content") @@ -1346,7 +1346,7 @@ def test_index_file_with_pages_and_transclusions(self, mock_file, mock_get_credi self.assertGreater(mock_file.call_count, 0) # Verify validate_and_write_tei_file was called for the index - mock_validate_write.assert_any_call("Complete index TEI", "torah") + mock_validate_write.assert_any_call("Complete index TEI", "torah", None) # Verify return value self.assertEqual(result, "Complete index TEI") @@ -1436,8 +1436,8 @@ def test_index_file_recursive_processing(self, mock_file, mock_get_credits, mock # Verify that book_file was called for each Book transclusion self.assertEqual(mock_book_file.call_count, 2) - mock_book_file.assert_any_call(self.test_book1, None) - mock_book_file.assert_any_call(self.test_book2, None) + mock_book_file.assert_any_call(self.test_book1, None, None) + mock_book_file.assert_any_call(self.test_book2, None, None) # Verify index_file was not called recursively (no Index transclusions) mock_index_file.assert_not_called() @@ -1479,10 +1479,10 @@ def test_index_file_with_nested_index(self, mock_file, mock_header, mock_tei_fil result = index_file(parent_index) # Verify that book_file was called for Book transclusion - mock_book_file.assert_called_once_with(self.test_book2, None) - + mock_book_file.assert_called_once_with(self.test_book2, None, None) + # Verify that index_file was called recursively for Index transclusion - mock_index_file.assert_called_once_with(child_index, None) + mock_index_file.assert_called_once_with(child_index, None, None) # Verify return value self.assertEqual(result, "Parent TEI") @@ -1752,6 +1752,37 @@ def test_mediawiki_xml_to_tei_tei_elements_preserved(self): # Note: The note element might not be in the main output due to XSLT processing +class TestMakeProjectDirectory(unittest.TestCase): + """Tests for make_project_directory.""" + + @patch("opensiddur.importer.jps1917.convert_wikisource.Path.mkdir") + def test_make_project_directory_custom_path(self, mock_mkdir): + explicit = Path("/custom/project/jps1917") + result = make_project_directory(explicit) + self.assertEqual(result, explicit.resolve()) + mock_mkdir.assert_called_once_with(parents=True, exist_ok=True) + + +class TestConvertWikisourceMain(unittest.TestCase): + """Tests for convert_wikisource main().""" + + @patch("opensiddur.importer.jps1917.convert_wikisource.index_file") + @patch("opensiddur.importer.jps1917.convert_wikisource.make_project_directory") + def test_main_uses_project_dir(self, mock_make_project_directory, mock_index_file): + mock_project_dir = Path("/mock/project/jps1917") + mock_sourcetexts_root = Path("/mock/sources") + mock_make_project_directory.return_value = mock_project_dir + + main(["--project-dir", str(mock_project_dir), "--sourcetexts-root", str(mock_sourcetexts_root)]) + + mock_make_project_directory.assert_called_once_with(mock_project_dir) + self.assertGreater(mock_index_file.call_count, 0) + for call in mock_index_file.call_args_list: + self.assertEqual(call.args[2], mock_project_dir) + self.assertEqual(call.kwargs.get("project_dir"), None) + self.assertEqual(call.args[1], mock_sourcetexts_root) + + if __name__ == '__main__': unittest.main()