Skip to content
Open
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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ All notable changes follow Keep a Changelog and Semantic Versioning.

## [Unreleased]

### Fixed

- `process_file` now rejects PDF, DOCX, XLSX, and PPTX immediately with `UnsupportedFormatError`
instead of extracting and processing the whole document before failing at rendering time with a
generic message.
- `inspect_file` now rejects an `encoding` argument for inspection-only formats instead of
silently ignoring it.
- `BuiltinFileAdapter` now rejects inspection-only formats at construction instead of decoding a
binary document as text.

## [0.3.0] - 2026-08-22

### Added
Expand Down
41 changes: 21 additions & 20 deletions src/pseudonymize/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,14 @@
FileProcessingError,
InvalidKeyError,
UnsupportedDataError,
UnsupportedFormatError,
)
from pseudonymize.formats import (
INSPECTION_ONLY_FORMATS,
BuiltinFileAdapter,
FileFormat,
select_file_format,
)
from pseudonymize.formats import BuiltinFileAdapter, FileFormat, select_file_format
from pseudonymize.policy import Policy
from pseudonymize.processing import (
DetectionReport,
Expand Down Expand Up @@ -468,19 +474,13 @@ def _processing_adapters(
) -> tuple[InputAdapter[Path], OutputAdapter]:
if input_adapter is None and output_adapter is None:
selected_format = select_file_format(source, format)
if selected_format == FileFormat.PDF:
from pseudonymize.inspection.pdf import PDFInspectionAdapter

pdf_adapter = PDFInspectionAdapter()
return pdf_adapter, pdf_adapter
elif selected_format in (FileFormat.DOCX, FileFormat.XLSX, FileFormat.PPTX):
from pseudonymize.inspection.office import OfficeInspectionAdapter

office_adapter = OfficeInspectionAdapter(selected_format)
return office_adapter, office_adapter
else:
builtin_adapter = BuiltinFileAdapter(selected_format, encoding)
return builtin_adapter, builtin_adapter
if selected_format in INSPECTION_ONLY_FORMATS:
raise UnsupportedFormatError(
f"{selected_format.value} supports inspection only; "
"use inspect_file or supply custom adapters"
)
builtin_adapter = BuiltinFileAdapter(selected_format, encoding)
return builtin_adapter, builtin_adapter
if input_adapter is None or output_adapter is None:
raise ValueError("custom file processing requires input and output adapters")
if format is not None or encoding is not None:
Expand All @@ -496,16 +496,17 @@ def _inspection_adapter(
) -> InputAdapter[Path]:
if input_adapter is None:
selected_format = select_file_format(source, format)
if selected_format == FileFormat.PDF:
from pseudonymize.inspection.pdf import PDFInspectionAdapter
if selected_format in INSPECTION_ONLY_FORMATS:
if encoding is not None:
raise ValueError("encoding applies only to text-based formats")
if selected_format is FileFormat.PDF:
from pseudonymize.inspection.pdf import PDFInspectionAdapter

return PDFInspectionAdapter()
elif selected_format in (FileFormat.DOCX, FileFormat.XLSX, FileFormat.PPTX):
return PDFInspectionAdapter()
from pseudonymize.inspection.office import OfficeInspectionAdapter

return OfficeInspectionAdapter(selected_format)
else:
return BuiltinFileAdapter(selected_format, encoding)
return BuiltinFileAdapter(selected_format, encoding)
if format is not None or encoding is not None:
raise ValueError("custom adapters cannot be combined with format or encoding")
return input_adapter
Expand Down
10 changes: 10 additions & 0 deletions src/pseudonymize/formats.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ class FileFormat(StrEnum):
PPTX = "pptx"


INSPECTION_ONLY_FORMATS = frozenset(
{FileFormat.PDF, FileFormat.DOCX, FileFormat.XLSX, FileFormat.PPTX}
)
_SUFFIX_FORMATS = {
".txt": FileFormat.TEXT,
".md": FileFormat.MARKDOWN,
Expand Down Expand Up @@ -73,6 +76,13 @@ class BuiltinFileAdapter:
encoding: str | None = None
_state: "_AdapterState | None" = field(default=None, init=False, repr=False)

def __post_init__(self) -> None:
if self.format in INSPECTION_ONLY_FORMATS:
raise UnsupportedFormatError(
"the built-in adapter handles text-based formats only; "
f"{self.format.value} is available through inspection"
)

def extract(self, source: Path) -> Document:
decoded = _decode(source.read_bytes(), self.encoding)
content: object
Expand Down
10 changes: 6 additions & 4 deletions tests/integration/test_inspection_adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,15 +127,17 @@ def test_inspect_pptx(test_pptx_path: Path) -> None:

def test_process_pdf_fails(test_pdf_path: Path, tmp_path: Path) -> None:
engine = Pseudonymizer(policy=Policy.default())
from pseudonymize.exceptions import AdapterExecutionError
from pseudonymize.exceptions import UnsupportedFormatError

with pytest.raises(AdapterExecutionError, match="output adapter failed during rendering"):
with pytest.raises(UnsupportedFormatError, match="inspection only"):
engine.process_file(test_pdf_path, tmp_path / "out.pdf", format=FileFormat.PDF)
assert not (tmp_path / "out.pdf").exists()


def test_process_docx_fails(test_docx_path: Path, tmp_path: Path) -> None:
engine = Pseudonymizer(policy=Policy.default())
from pseudonymize.exceptions import AdapterExecutionError
from pseudonymize.exceptions import UnsupportedFormatError

with pytest.raises(AdapterExecutionError, match="output adapter failed during rendering"):
with pytest.raises(UnsupportedFormatError, match="inspection only"):
engine.process_file(test_docx_path, tmp_path / "out.docx", format=FileFormat.DOCX)
assert not (tmp_path / "out.docx").exists()
32 changes: 31 additions & 1 deletion tests/unit/test_inspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@
import pytest

from pseudonymize.document import Document
from pseudonymize.exceptions import AdapterContractError, AdapterExecutionError
from pseudonymize.engine import Pseudonymizer
from pseudonymize.exceptions import (
AdapterContractError,
AdapterExecutionError,
UnsupportedFormatError,
)
from pseudonymize.formats import BuiltinFileAdapter, FileFormat
from pseudonymize.inspection import office, pdf
from pseudonymize.inspection.office import OfficeInspectionAdapter
from pseudonymize.inspection.pdf import PDFInspectionAdapter
Expand Down Expand Up @@ -58,3 +64,27 @@ def test_office_adapter_render_contract() -> None:
adapter = OfficeInspectionAdapter("docx")
with pytest.raises(AdapterContractError, match="inspection only"):
adapter.render(Document("test", (), {}))


@pytest.mark.parametrize("suffix", [".pdf", ".docx", ".xlsx", ".pptx"])
def test_process_file_rejects_inspection_only_formats_before_reading(
tmp_path: Path, suffix: str
) -> None:
source = tmp_path / f"document{suffix}"
source.write_bytes(b"never read")
with pytest.raises(UnsupportedFormatError, match="inspection only"):
Pseudonymizer().process_file(source, tmp_path / f"output{suffix}")
assert not (tmp_path / f"output{suffix}").exists()


@pytest.mark.parametrize("format", ["pdf", "docx", "xlsx", "pptx"])
def test_builtin_adapter_rejects_inspection_only_formats(format: str) -> None:
with pytest.raises(UnsupportedFormatError, match="text-based formats only"):
BuiltinFileAdapter(FileFormat(format))


def test_inspect_file_rejects_encoding_for_inspection_only_formats(tmp_path: Path) -> None:
source = tmp_path / "document.pdf"
source.write_bytes(b"never read")
with pytest.raises(ValueError, match="text-based formats"):
Pseudonymizer().inspect_file(source, encoding="utf-8")