diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..daad4f0d --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,28 @@ +# Copilot Instructions for pretext-cli + +Prefer running project commands with `poetry run` so they use the repository's +configured Python environment and tool versions. + +When working on an assigned GitHub issue and preparing to open a pull request, +run code formatting before creating the PR: + +```bash +poetry run black . +``` + +Only open the PR after formatting has been run successfully. + +When changes affect behavior in `pretext/`, add or update tests in `tests/` +that cover the change. + +Prefer test-driven development whenever practical: write or update tests first, +then implement the code change to satisfy them. + +Before opening the PR, run relevant tests (or the full suite when needed): + +```bash +poetry run pytest +``` + +For user-visible changes, add an entry under `[Unreleased]` in `CHANGELOG.md` +using Keep a Changelog categories (Added, Changed, Fixed, Removed). diff --git a/pretext/project/__init__.py b/pretext/project/__init__.py index 73838c40..43f40a37 100644 --- a/pretext/project/__init__.py +++ b/pretext/project/__init__.py @@ -61,6 +61,16 @@ class Format(str, Enum): CUSTOM = "custom" +# Maps single-file output formats to their file extension, used when looking up +# the actual output file to generate a better deploy link. +_SINGLE_FILE_FORMAT_EXTENSIONS: t.Dict["Format", str] = { + Format.PDF: ".pdf", + Format.EPUB: ".epub", + Format.KINDLE: ".epub", + Format.BRAILLE: ".brl", +} + + # The CLI only needs two values from the publication file. Therefore, this class ignores the vast majority of a publication file's contents, loading and validating only a (small) relevant subset. # Since we will want to hash the baseurl for generating qr codes, we also load it here. class PublicationSubset( @@ -439,9 +449,19 @@ def deploy_dir_relpath(self) -> Path: return self._project.stage / self.deploy_dir_path() def deploy_path(self) -> Path: - if self.output_filename is None: - return self.deploy_dir_path() - return self.deploy_dir_path() / self.output_filename + if self.output_filename is not None: + return self.deploy_dir_path() / self.output_filename + # For single-file output formats, look for the actual output file in + # the output directory to create a better link in the pelican-generated site. + ext = _SINGLE_FILE_FORMAT_EXTENSIONS.get(self.format) + if ext is not None: + output_dir = self.output_dir_abspath() + if output_dir.exists(): + gen = output_dir.glob(f"*{ext}") + first = next(gen, None) + if first is not None and next(gen, None) is None: + return self.deploy_dir_path() / first.name + return self.deploy_dir_path() def xsl_abspath(self) -> t.Optional[Path]: if self.xsl is None: diff --git a/tests/test_project.py b/tests/test_project.py index 19dddb83..9e591da1 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -416,6 +416,52 @@ def test_deploy(tmp_path: Path) -> None: ) +def test_deploy_path(tmp_path: Path) -> None: + # Test that deploy_path() returns the correct path for single-file formats + # when output_filename is not specified. + prj_path = tmp_path / "test_deploy_path" + shutil.copytree(EXAMPLES_DIR / "projects" / "project_refactor" / "simple", prj_path) + (prj_path / "project.ptx").unlink() + with utils.working_directory(prj_path): + project = pr.Project(ptx_version="2") + + # For PDF target with no output_filename and no output dir: fallback to directory + t_pdf = project.new_target(name="print", format="pdf", deploy_dir="print-dir") + assert t_pdf.deploy_path() == Path("print-dir") + + # Simulate a built PDF in the output directory + pdf_output_dir = t_pdf.output_dir_abspath() + pdf_output_dir.mkdir(parents=True, exist_ok=True) + (pdf_output_dir / "main.pdf").touch() + + # deploy_path() should now find the PDF and include it in the path + assert t_pdf.deploy_path() == Path("print-dir") / "main.pdf" + + # If output_filename is explicitly set, it should take precedence + t_pdf_explicit = project.new_target( + name="print-explicit", + format="pdf", + deploy_dir="print-dir2", + output_filename="custom.pdf", + ) + assert t_pdf_explicit.deploy_path() == Path("print-dir2") / "custom.pdf" + + # For HTML format (directory output), deploy_path() should still return the directory + t_html = project.new_target(name="web", format="html", deploy_dir="web-dir") + assert t_html.deploy_path() == Path("web-dir") + + # EPUB target with no output file: fallback to directory + t_epub = project.new_target(name="epub", format="epub", deploy_dir="epub-dir") + assert t_epub.deploy_path() == Path("epub-dir") + + # Simulate a built EPUB + epub_output_dir = t_epub.output_dir_abspath() + epub_output_dir.mkdir(parents=True, exist_ok=True) + (epub_output_dir / "book.epub").touch() + + assert t_epub.deploy_path() == Path("epub-dir") / "book.epub" + + def test_validation(tmp_path: Path) -> None: project = pr.Project(ptx_version="2") # Verify that repeated server names cause a validation error.