Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -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).
26 changes: 23 additions & 3 deletions pretext/project/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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:
Expand Down
46 changes: 46 additions & 0 deletions tests/test_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading