diff --git a/.github/workflows/project-sync-safety.yml b/.github/workflows/project-sync-safety.yml
index 9362a5f..a7f90b0 100644
--- a/.github/workflows/project-sync-safety.yml
+++ b/.github/workflows/project-sync-safety.yml
@@ -41,10 +41,10 @@ jobs:
steps:
- name: Check out repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
- name: Set up Python
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v7
with:
python-version: "3.12"
cache: pip
@@ -54,7 +54,7 @@ jobs:
- name: Check focused formatting
run: >-
- python -m black --check --diff
+ python -m ruff format --check
src/docmergeforge/project/sync.py
src/docmergeforge/project/drift.py
src/docmergeforge/project/discovery.py
diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml
index 78e9980..07fb237 100644
--- a/.github/workflows/quality.yml
+++ b/.github/workflows/quality.yml
@@ -31,8 +31,9 @@ jobs:
- run: python -m pip install --upgrade pip
- run: pip install -e ".[dev,web]"
- run: pre-commit validate-config
+ # Keep CI formatting aligned with the pinned Ruff pre-commit policy.
- run: ruff check .
- - run: black --check --diff .
+ - run: ruff format --check .
- run: mypy src/docmergeforge
- run: python scripts/check_docs_links.py
- run: python scripts/check_repository_reference.py
diff --git a/.github/workflows/regression.yml b/.github/workflows/regression.yml
index 0199990..e7e0f68 100644
--- a/.github/workflows/regression.yml
+++ b/.github/workflows/regression.yml
@@ -28,7 +28,7 @@ jobs:
sudo apt-get update
sudo apt-get install -y libegl1
- run: python -m pip install --upgrade pip
- - run: pip install -e ".[dev]"
+ - run: pip install -e ".[dev,web]"
- run: python scripts/generate_120_fixture.py fixtures/generated/sql-120
- run: pytest -m "regression or integration" tests/regression tests/integration
- run: docmergeforge validate --input fixtures/generated/sql-120 --parts 1-120
diff --git a/docs/build/release-checklist.md b/docs/build/release-checklist.md
index f293d07..b7390e7 100644
--- a/docs/build/release-checklist.md
+++ b/docs/build/release-checklist.md
@@ -26,7 +26,7 @@ At the intended release commit:
- [ ] Quality workflow green.
- [ ] `pre-commit validate-config` green.
- [ ] Ruff green.
-- [ ] Black green.
+- [ ] Ruff formatting green.
- [ ] strict mypy green.
- [ ] repository-local Markdown link integrity green.
- [ ] full pytest green.
diff --git a/docs/history/what_changed-through-2026-08-20-cross-platform.md b/docs/history/what_changed-through-2026-08-20-cross-platform.md
index 9b1e0e7..79af9ea 100644
--- a/docs/history/what_changed-through-2026-08-20-cross-platform.md
+++ b/docs/history/what_changed-through-2026-08-20-cross-platform.md
@@ -1,6 +1,6 @@
# What Changed
-This file records the current DocMergeForge development pass, verification evidence, and remaining release gates. Earlier detailed development history is preserved in [`docs/history/what_changed-through-2026-08-18.md`](docs/history/what_changed-through-2026-08-18.md) so this top-level record stays readable instead of growing without bound.
+This file records the current DocMergeForge development pass, verification evidence, and remaining release gates. Earlier detailed development history is preserved in [`what_changed-through-2026-08-18.md`](what_changed-through-2026-08-18.md) so this top-level record stays readable instead of growing without bound.
An item is not treated as finished merely because code was pushed. CI, packaging, platform acceptance, external-office fidelity evidence, accessibility review, and release-signing evidence remain separate completion gates.
diff --git a/pyproject.toml b/pyproject.toml
index 263c34d..1dc4937 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -46,8 +46,7 @@ Funding = "https://buymeacoffee.com/sanskarIN"
dev = [
"pytest>=8.3",
"pytest-cov>=6",
- "ruff>=0.9",
- "black>=25.1",
+ "ruff==0.16.3",
"mypy>=1.15",
"pre-commit>=4",
"types-setuptools",
@@ -73,6 +72,7 @@ packages = ["src/docmergeforge"]
[tool.pytest.ini_options]
testpaths = ["tests"]
+pythonpath = ["."]
addopts = "-q --strict-markers"
markers = [
"integration: tests requiring document libraries",
@@ -86,9 +86,9 @@ target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP", "SIM", "C4"]
-[tool.black]
-line-length = 100
-target-version = ["py312"]
+[tool.ruff.lint.per-file-ignores]
+"src/docmergeforge/ui/main.py" = ["SIM103"]
+"src/docmergeforge/web/app.py" = ["E501"]
[tool.mypy]
python_version = "3.12"
diff --git a/scripts/check_docx_fidelity_acceptance.py b/scripts/check_docx_fidelity_acceptance.py
index ab3e0c3..6763c9b 100644
--- a/scripts/check_docx_fidelity_acceptance.py
+++ b/scripts/check_docx_fidelity_acceptance.py
@@ -13,9 +13,7 @@ def build_fixture(path: Path) -> None:
document = Document()
document.core_properties.title = "DocMergeForge Fidelity Acceptance"
document.add_heading("Fidelity Acceptance", level=1)
- document.add_paragraph(
- "Representative smoke content with bold, italic, and list formatting."
- )
+ document.add_paragraph("Representative smoke content with bold, italic, and list formatting.")
formatted = document.add_paragraph()
formatted.add_run("Bold text").bold = True
formatted.add_run(" and ")
diff --git a/scripts/generate_120_fixture.py b/scripts/generate_120_fixture.py
index 4125fbe..207f964 100644
--- a/scripts/generate_120_fixture.py
+++ b/scripts/generate_120_fixture.py
@@ -47,8 +47,7 @@ def main() -> int:
zf.writestr("example.sql", f"-- Part {part}\nSELECT {part};\n")
print(
- "Generated 120 PDF, 120 DOCX, and 120 independent companion ZIP fixtures "
- f"in {args.output}"
+ f"Generated 120 PDF, 120 DOCX, and 120 independent companion ZIP fixtures in {args.output}"
)
return 0
diff --git a/src/docmergeforge/cli/main.py b/src/docmergeforge/cli/main.py
index 65b6c8f..480a58d 100644
--- a/src/docmergeforge/cli/main.py
+++ b/src/docmergeforge/cli/main.py
@@ -278,9 +278,7 @@ def _run_direct_merge(args: argparse.Namespace) -> int:
"ready": False,
"missing": validation_result.missing_parts,
"duplicates": validation_result.duplicate_parts,
- "diagnostics": [
- item.to_dict() for item in validation_result.diagnostics
- ],
+ "diagnostics": [item.to_dict() for item in validation_result.diagnostics],
},
indent=2,
)
diff --git a/src/docmergeforge/diagnostics/logging.py b/src/docmergeforge/diagnostics/logging.py
index 5acbb9d..f4b885a 100644
--- a/src/docmergeforge/diagnostics/logging.py
+++ b/src/docmergeforge/diagnostics/logging.py
@@ -15,9 +15,7 @@
r"(?P=quote)?"
)
_BEARER_PATTERN = re.compile(r"(?i)\bBearer\s+[A-Za-z0-9._~+/=-]+")
-_AUTH_HEADER_PATTERN = re.compile(
- r"(?i)\bAuthorization\s*:\s*(?:Basic|Bearer)\s+[^\s,;]+"
-)
+_AUTH_HEADER_PATTERN = re.compile(r"(?i)\bAuthorization\s*:\s*(?:Basic|Bearer)\s+[^\s,;]+")
_API_KEY_HEADER_PATTERN = re.compile(r"(?i)\b(?:X-)?Api-Key\s*:\s*[^\s,;]+")
@@ -57,13 +55,14 @@ def configure_logging(path: Path, level: str = "INFO") -> logging.Logger:
logger.setLevel(getattr(logging, level.upper(), logging.INFO))
logger.propagate = False
- for handler in list(logger.handlers):
- handler.close()
- logger.removeHandler(handler)
+ for existing_handler in list(logger.handlers):
+ existing_handler.close()
+ logger.removeHandler(existing_handler)
+ handler: logging.Handler
try:
path.parent.mkdir(parents=True, exist_ok=True)
- handler: logging.Handler = RotatingFileHandler(
+ handler = RotatingFileHandler(
path,
maxBytes=5 * 1024 * 1024,
backupCount=3,
diff --git a/src/docmergeforge/discovery/scanner.py b/src/docmergeforge/discovery/scanner.py
index e5f099f..51abc7a 100644
--- a/src/docmergeforge/discovery/scanner.py
+++ b/src/docmergeforge/discovery/scanner.py
@@ -58,9 +58,7 @@ def _iter_directory(
for directory, directory_names, file_names in os.walk(root, followlinks=False):
directory_path = Path(directory)
directory_names[:] = [
- name
- for name in directory_names
- if not _is_excluded(directory_path / name, excluded)
+ name for name in directory_names if not _is_excluded(directory_path / name, excluded)
]
for name in file_names:
path = directory_path / name
diff --git a/src/docmergeforge/docx/engine.py b/src/docmergeforge/docx/engine.py
index d67f067..39edd56 100644
--- a/src/docmergeforge/docx/engine.py
+++ b/src/docmergeforge/docx/engine.py
@@ -132,7 +132,7 @@ def merge(
diagnostics = validate_docx_package(temporary)
if any(diag.level.value in {"ERROR", "FATAL"} for diag in diagnostics):
raise ValidationError(
- "Output DOCX package validation failed: " f"{diagnostics[0].message}"
+ f"Output DOCX package validation failed: {diagnostics[0].message}"
)
Document(str(temporary))
diff --git a/src/docmergeforge/docx/fidelity_acceptance.py b/src/docmergeforge/docx/fidelity_acceptance.py
index 3ed5140..d8df50a 100644
--- a/src/docmergeforge/docx/fidelity_acceptance.py
+++ b/src/docmergeforge/docx/fidelity_acceptance.py
@@ -128,12 +128,8 @@ def snapshot_docx_structure(path: Path) -> DocxStructureSnapshot:
inline_shapes=len(document.inline_shapes),
sections=len(document.sections),
headings=headings,
- header_paragraphs=sum(
- len(section.header.paragraphs) for section in document.sections
- ),
- footer_paragraphs=sum(
- len(section.footer.paragraphs) for section in document.sections
- ),
+ header_paragraphs=sum(len(section.header.paragraphs) for section in document.sections),
+ footer_paragraphs=sum(len(section.footer.paragraphs) for section in document.sections),
header_tables=sum(len(section.header.tables) for section in document.sections),
footer_tables=sum(len(section.footer.tables) for section in document.sections),
)
@@ -152,9 +148,7 @@ def snapshot_docx_content(path: Path) -> DocxContentSnapshot:
footer_texts.extend(_table_texts(section.footer.tables))
return DocxContentSnapshot(
- body_paragraphs_sha256=_digest_texts(
- paragraph.text for paragraph in document.paragraphs
- ),
+ body_paragraphs_sha256=_digest_texts(paragraph.text for paragraph in document.paragraphs),
tables_sha256=_digest_texts(_table_texts(document.tables)),
headers_sha256=_digest_texts(header_texts),
footers_sha256=_digest_texts(footer_texts),
diff --git a/src/docmergeforge/docx/fidelity_corpus.py b/src/docmergeforge/docx/fidelity_corpus.py
index 2ed4065..1b12f6c 100644
--- a/src/docmergeforge/docx/fidelity_corpus.py
+++ b/src/docmergeforge/docx/fidelity_corpus.py
@@ -67,11 +67,7 @@ def stopped_early(self) -> bool:
@property
def accepted(self) -> bool:
- return (
- self.discovered_count > 0
- and not self.stopped_early
- and self.failed_count == 0
- )
+ return self.discovered_count > 0 and not self.stopped_early and self.failed_count == 0
def to_dict(self) -> dict[str, Any]:
return {
diff --git a/src/docmergeforge/docx/libreoffice_uno_acceptance.py b/src/docmergeforge/docx/libreoffice_uno_acceptance.py
index 8940693..6c5507f 100644
--- a/src/docmergeforge/docx/libreoffice_uno_acceptance.py
+++ b/src/docmergeforge/docx/libreoffice_uno_acceptance.py
@@ -227,9 +227,7 @@ def run_libreoffice_uno_acceptance(
capability = require_fidelity_automation("libreoffice")
uno_python = find_uno_python()
if uno_python is None:
- raise ValidationError(
- "LibreOffice UNO acceptance requires a Python UNO bridge."
- )
+ raise ValidationError("LibreOffice UNO acceptance requires a Python UNO bridge.")
libreoffice_uno_merge_documents(
ordered,
output,
diff --git a/src/docmergeforge/docx/libreoffice_uno_merge.py b/src/docmergeforge/docx/libreoffice_uno_merge.py
index 470748f..1eb165d 100644
--- a/src/docmergeforge/docx/libreoffice_uno_merge.py
+++ b/src/docmergeforge/docx/libreoffice_uno_merge.py
@@ -20,7 +20,7 @@
)
from docmergeforge.utilities.hashing import sha256_file
-_UNO_WORKER = r'''
+_UNO_WORKER = r"""
from __future__ import annotations
import argparse
@@ -142,7 +142,7 @@ def main() -> int:
if __name__ == "__main__":
raise SystemExit(main())
-'''.strip()
+""".strip()
@dataclass(slots=True, frozen=True)
@@ -371,9 +371,7 @@ def libreoffice_uno_merge_documents(
text=True,
)
try:
- worker_stdout, worker_stderr = worker_process.communicate(
- timeout=timeout_seconds
- )
+ worker_stdout, worker_stderr = worker_process.communicate(timeout=timeout_seconds)
except subprocess.TimeoutExpired as exc:
worker_process.kill()
worker_stdout, worker_stderr = worker_process.communicate()
@@ -383,8 +381,7 @@ def libreoffice_uno_merge_documents(
if worker_process.returncode != 0:
detail = worker_stderr.strip() or worker_stdout.strip()
raise ValidationError(
- "LibreOffice UNO merge worker failed"
- + (f": {detail}" if detail else ".")
+ "LibreOffice UNO merge worker failed" + (f": {detail}" if detail else ".")
)
finally:
if worker_process is not None and worker_process.poll() is None:
diff --git a/src/docmergeforge/docx/section_evidence.py b/src/docmergeforge/docx/section_evidence.py
index f0b3022..d4e0bad 100644
--- a/src/docmergeforge/docx/section_evidence.py
+++ b/src/docmergeforge/docx/section_evidence.py
@@ -68,9 +68,7 @@ def page_number_section_records(path: Path) -> tuple[PageNumberSectionRecord, ..
try:
document_xml = archive.read(_DOCUMENT_XML)
except KeyError as exc:
- raise ValidationError(
- f"DOCX package is missing {_DOCUMENT_XML}: {path}"
- ) from exc
+ raise ValidationError(f"DOCX package is missing {_DOCUMENT_XML}: {path}") from exc
except zipfile.BadZipFile as exc:
raise ValidationError(f"Invalid DOCX ZIP container: {path}") from exc
@@ -118,8 +116,6 @@ def page_number_properties_sha256(paths: Sequence[Path]) -> str:
global_section_index = 0
for path in paths:
for record in page_number_section_records(path):
- canonical_records.append(
- f"section={global_section_index}|{record.canonical()}"
- )
+ canonical_records.append(f"section={global_section_index}|{record.canonical()}")
global_section_index += 1
return _digest_records(canonical_records)
diff --git a/src/docmergeforge/docx/word.py b/src/docmergeforge/docx/word.py
index a1fe3ef..b48b0eb 100644
--- a/src/docmergeforge/docx/word.py
+++ b/src/docmergeforge/docx/word.py
@@ -70,10 +70,10 @@ def word_roundtrip_copy(
raise ValidationError("Microsoft Word fidelity round-trip accepts DOCX paths only.")
if not source.exists() or not source.is_file():
raise FileNotFoundError(source)
- if destination.exists():
- raise FileExistsError(f"Refusing to overwrite existing DOCX output: {destination}")
if source.resolve() == destination.resolve():
raise ValidationError("Microsoft Word fidelity round-trip requires a separate output path.")
+ if destination.exists():
+ raise FileExistsError(f"Refusing to overwrite existing DOCX output: {destination}")
host = powershell or find_word_powershell_host()
if host is None:
diff --git a/src/docmergeforge/docx/word_merge_acceptance.py b/src/docmergeforge/docx/word_merge_acceptance.py
index 84e8523..1e42023 100644
--- a/src/docmergeforge/docx/word_merge_acceptance.py
+++ b/src/docmergeforge/docx/word_merge_acceptance.py
@@ -130,9 +130,7 @@ def _table_texts(tables: Iterable[Any]) -> list[str]:
def _header_texts(document: Any) -> list[str]:
values: list[str] = []
for section in document.sections:
- values.extend(
- paragraph.text for paragraph in section.header.paragraphs if paragraph.text
- )
+ values.extend(paragraph.text for paragraph in section.header.paragraphs if paragraph.text)
values.extend(_table_texts(section.header.tables))
return values
@@ -140,9 +138,7 @@ def _header_texts(document: Any) -> list[str]:
def _footer_texts(document: Any) -> list[str]:
values: list[str] = []
for section in document.sections:
- values.extend(
- paragraph.text for paragraph in section.footer.paragraphs if paragraph.text
- )
+ values.extend(paragraph.text for paragraph in section.footer.paragraphs if paragraph.text)
values.extend(_table_texts(section.footer.tables))
return values
@@ -181,18 +177,13 @@ def _section_record(section: Any) -> str:
f"gutter={_scalar(section.gutter)}",
f"header_distance={_scalar(section.header_distance)}",
f"footer_distance={_scalar(section.footer_distance)}",
- "different_first_page="
- f"{int(bool(section.different_first_page_header_footer))}",
+ f"different_first_page={int(bool(section.different_first_page_header_footer))}",
f"header_linked={int(bool(section.header.is_linked_to_previous))}",
- "first_header_linked="
- f"{int(bool(section.first_page_header.is_linked_to_previous))}",
- "even_header_linked="
- f"{int(bool(section.even_page_header.is_linked_to_previous))}",
+ f"first_header_linked={int(bool(section.first_page_header.is_linked_to_previous))}",
+ f"even_header_linked={int(bool(section.even_page_header.is_linked_to_previous))}",
f"footer_linked={int(bool(section.footer.is_linked_to_previous))}",
- "first_footer_linked="
- f"{int(bool(section.first_page_footer.is_linked_to_previous))}",
- "even_footer_linked="
- f"{int(bool(section.even_page_footer.is_linked_to_previous))}",
+ f"first_footer_linked={int(bool(section.first_page_footer.is_linked_to_previous))}",
+ f"even_footer_linked={int(bool(section.even_page_footer.is_linked_to_previous))}",
)
)
@@ -205,9 +196,7 @@ def _section_properties_sha256(paths: Sequence[Path]) -> str:
for path in paths:
document = Document(str(path))
for section in document.sections:
- records.append(
- f"section={global_section_index}|{_section_record(section)}"
- )
+ records.append(f"section={global_section_index}|{_section_record(section)}")
global_section_index += 1
return _digest_texts(records)
@@ -315,9 +304,7 @@ def _validate_acceptance_inputs(
return ordered_sources
-def _verify_source_hashes(
- sources: Sequence[Path], source_hashes: Sequence[str]
-) -> None:
+def _verify_source_hashes(sources: Sequence[Path], source_hashes: Sequence[str]) -> None:
for source, expected_hash in zip(sources, source_hashes, strict=True):
verify_native_source_unchanged(source, expected_hash)
diff --git a/src/docmergeforge/project/store.py b/src/docmergeforge/project/store.py
index caf36f6..7800351 100644
--- a/src/docmergeforge/project/store.py
+++ b/src/docmergeforge/project/store.py
@@ -72,9 +72,8 @@ def _required_string(data: dict[str, Any], key: str) -> str:
def _path_list(value: object, label: str, *, allow_empty: bool) -> list[Path]:
- if (
- not isinstance(value, list)
- or not all(isinstance(item, str) and item.strip() for item in value)
+ if not isinstance(value, list) or not all(
+ isinstance(item, str) and item.strip() for item in value
):
raise ValueError(f"Project field '{label}' must be a JSON array of non-empty path strings.")
if not allow_empty and not value:
@@ -167,9 +166,7 @@ def _pdf_settings(data: dict[str, Any]) -> PdfSettings:
def _docx_settings(data: dict[str, Any]) -> DocxSettings:
label = "settings.docx"
return DocxSettings(
- start_each_part_on_new_page=_bool_value(
- data, "start_each_part_on_new_page", True, label
- ),
+ start_each_part_on_new_page=_bool_value(data, "start_each_part_on_new_page", True, label),
preserve_sections=_bool_value(data, "preserve_sections", True, label),
fidelity_mode=_choice(
data,
@@ -196,9 +193,7 @@ def _docx_settings(data: dict[str, Any]) -> DocxSettings:
),
header_text=_optional_string(data, "header_text", label),
footer_text=_optional_string(data, "footer_text", label),
- continuous_page_numbering=_bool_value(
- data, "continuous_page_numbering", True, label
- ),
+ continuous_page_numbering=_bool_value(data, "continuous_page_numbering", True, label),
)
@@ -223,16 +218,10 @@ def _project_from_raw(raw: object) -> MergeProject:
settings = MergeSettings(
expected_start=expected_start,
expected_end=expected_end,
- checksum_generation=_bool_value(
- settings_data, "checksum_generation", True, "settings"
- ),
- automatic_validation=_bool_value(
- settings_data, "automatic_validation", True, "settings"
- ),
+ checksum_generation=_bool_value(settings_data, "checksum_generation", True, "settings"),
+ automatic_validation=_bool_value(settings_data, "automatic_validation", True, "settings"),
overwrite=_bool_value(settings_data, "overwrite", False, "settings"),
- profile_name=_string_value(
- settings_data, "profile_name", "Exact Preservation", "settings"
- ),
+ profile_name=_string_value(settings_data, "profile_name", "Exact Preservation", "settings"),
filename_template=_string_value(
settings_data, "filename_template", "{series}_Master", "settings"
),
diff --git a/src/docmergeforge/project/sync.py b/src/docmergeforge/project/sync.py
index f9683d2..6d4f371 100644
--- a/src/docmergeforge/project/sync.py
+++ b/src/docmergeforge/project/sync.py
@@ -78,7 +78,10 @@ def to_dict(self) -> dict[str, object]:
}
-def _eligible_documents(project: MergeProject, discovered: list[InputDocument]) -> list[InputDocument]:
+def _eligible_documents(
+ project: MergeProject,
+ discovered: list[InputDocument],
+) -> list[InputDocument]:
start = project.settings.expected_start
end = project.settings.expected_end
unique: list[InputDocument] = []
@@ -127,9 +130,7 @@ def _missing_parts(
end: int,
) -> tuple[int, ...]:
found = {
- item.part.number
- for item in documents
- if item.kind == kind and item.part.number is not None
+ item.part.number for item in documents if item.kind == kind and item.part.number is not None
}
if not found:
return ()
@@ -157,9 +158,7 @@ def plan_project_sync(
proposed_set = set(proposed_keys)
added = tuple(
- path
- for path, key in zip(proposed, proposed_keys, strict=True)
- if key not in current_set
+ path for path, key in zip(proposed, proposed_keys, strict=True) if key not in current_set
)
removed = tuple(
path for path, key in zip(current, current_keys, strict=True) if key not in proposed_set
diff --git a/src/docmergeforge/ui/desktop_entry.py b/src/docmergeforge/ui/desktop_entry.py
index dfea376..085d0af 100644
--- a/src/docmergeforge/ui/desktop_entry.py
+++ b/src/docmergeforge/ui/desktop_entry.py
@@ -32,7 +32,8 @@ def __init__(self) -> None:
self.sync_project_button = QPushButton("Synchronize Project Sources")
self.sync_project_button.setAccessibleName("Synchronize project sources")
self.sync_project_button.setAccessibleDescription(
- "Browse for a saved project, preview source-selection changes, and optionally apply them."
+ "Browse for a saved project, preview source-selection changes, "
+ "and optionally apply them."
)
self.sync_project_button.setMinimumHeight(58)
self.sync_project_button.clicked.connect(self._synchronize_project)
diff --git a/src/docmergeforge/ui/project_sync_dialog.py b/src/docmergeforge/ui/project_sync_dialog.py
index f19b734..20840a2 100644
--- a/src/docmergeforge/ui/project_sync_dialog.py
+++ b/src/docmergeforge/ui/project_sync_dialog.py
@@ -61,7 +61,9 @@ def __init__(self, project_path: Path, plan: ProjectSyncPlan) -> None:
"make the automatic selection ambiguous. Resolve the duplicates and preview again."
)
elif not plan.changed:
- guidance = "The saved selected-file list already matches the current automatic proposal."
+ guidance = (
+ "The saved selected-file list already matches the current automatic proposal."
+ )
elif plan.removed:
guidance = (
"Review the removals carefully. Applying this proposal requires a separate removal "
@@ -69,8 +71,8 @@ def __init__(self, project_path: Path, plan: ProjectSyncPlan) -> None:
)
else:
guidance = (
- "Review the complete proposal before applying it. A versioned backup of the project "
- "JSON will be created before the guarded update."
+ "Review the complete proposal before applying it. A versioned backup of the "
+ "project JSON will be created before the guarded update."
)
self.guidance = QLabel(guidance)
diff --git a/src/docmergeforge/validation/service.py b/src/docmergeforge/validation/service.py
index b9e8923..6484ca2 100644
--- a/src/docmergeforge/validation/service.py
+++ b/src/docmergeforge/validation/service.py
@@ -46,9 +46,7 @@ def validate_part_set(
)
)
if kind == DocumentKind.PDF and item.encrypted and is_merge_input:
- part_label = (
- f"Part {item.part.number}" if item.part.number is not None else "Selected"
- )
+ part_label = f"Part {item.part.number}" if item.part.number is not None else "Selected"
if allow_encrypted_pdf:
diagnostics.append(
Diagnostic(
diff --git a/src/docmergeforge/web/app.py b/src/docmergeforge/web/app.py
index 1d534d5..b8bcb46 100644
--- a/src/docmergeforge/web/app.py
+++ b/src/docmergeforge/web/app.py
@@ -200,9 +200,7 @@ def ordered_documents(input_root: Path) -> tuple[DocumentKind, list[InputDocumen
"""Discover one homogeneous PDF/DOCX upload set and order numbered parts naturally."""
documents = [
- item
- for item in scan([input_root])
- if item.kind in {DocumentKind.PDF, DocumentKind.DOCX}
+ item for item in scan([input_root]) if item.kind in {DocumentKind.PDF, DocumentKind.DOCX}
]
if not documents:
raise ValueError("No PDF or DOCX files were uploaded.")
@@ -425,4 +423,4 @@ async def merge(
detail="Merge failed. Check the DocMergeForge host logs for details.",
) from exc
- return app
\ No newline at end of file
+ return app
diff --git a/tests/integration/test_lo_uno_process_group.py b/tests/integration/test_lo_uno_process_group.py
index 2e0dd43..a79e2a1 100644
--- a/tests/integration/test_lo_uno_process_group.py
+++ b/tests/integration/test_lo_uno_process_group.py
@@ -7,7 +7,6 @@
from docmergeforge.docx import libreoffice_uno_merge
-
pytestmark = pytest.mark.skipif(
os.name != "posix",
reason="LibreOffice UNO process-group acceptance currently uses POSIX semantics.",
diff --git a/tests/integration/test_lo_uno_supervised_smoke.py b/tests/integration/test_lo_uno_supervised_smoke.py
index af5bc39..b255d9c 100644
--- a/tests/integration/test_lo_uno_supervised_smoke.py
+++ b/tests/integration/test_lo_uno_supervised_smoke.py
@@ -60,9 +60,7 @@ def fake_acceptance(
first_doc = Document(str(first))
second_doc = Document(str(second))
payload = json.loads(
- (output_dir / "libreoffice-uno-merge-evidence.json").read_text(
- encoding="utf-8"
- )
+ (output_dir / "libreoffice-uno-merge-evidence.json").read_text(encoding="utf-8")
)
assert exit_code == 0
diff --git a/tests/integration/test_word_native_merge_acceptance_script.py b/tests/integration/test_word_native_merge_acceptance_script.py
index 85e01fc..b5bf876 100644
--- a/tests/integration/test_word_native_merge_acceptance_script.py
+++ b/tests/integration/test_word_native_merge_acceptance_script.py
@@ -61,9 +61,7 @@ def fake_acceptance(
timeout_seconds: int,
start_each_on_new_page: bool,
) -> WordMergeAcceptanceEvidence:
- calls.append(
- (sources, destination, timeout_seconds, start_each_on_new_page)
- )
+ calls.append((sources, destination, timeout_seconds, start_each_on_new_page))
return _accepted_evidence(destination)
monkeypatch.setattr(script, "run_word_merge_acceptance", fake_acceptance)
diff --git a/tests/integration/test_word_timeout_cleanup_acceptance_script.py b/tests/integration/test_word_timeout_cleanup_acceptance_script.py
index a163562..130fd8d 100644
--- a/tests/integration/test_word_timeout_cleanup_acceptance_script.py
+++ b/tests/integration/test_word_timeout_cleanup_acceptance_script.py
@@ -34,9 +34,7 @@ def test_word_timeout_cleanup_acceptance_records_timeout_and_cleanup(
lambda mode: _capability(),
)
- def fake_run(
- command: list[str], *, timeout_seconds: int
- ) -> NativeCommandResult:
+ def fake_run(command: list[str], *, timeout_seconds: int) -> NativeCommandResult:
nonlocal captured_script
identity = Path(command[command.index("-ProcessIdentityFile") + 1])
identity.write_text(
@@ -49,9 +47,7 @@ def fake_run(
),
encoding="utf-8",
)
- captured_script = Path(command[command.index("-File") + 1]).read_text(
- encoding="utf-8"
- )
+ captured_script = Path(command[command.index("-File") + 1]).read_text(encoding="utf-8")
raise ValidationError(
f"Native DOCX fidelity command timed out after {timeout_seconds} seconds."
)
@@ -78,9 +74,7 @@ def fake_run(
]
)
payload = json.loads(
- (output_dir / "word-timeout-cleanup-evidence.json").read_text(
- encoding="utf-8"
- )
+ (output_dir / "word-timeout-cleanup-evidence.json").read_text(encoding="utf-8")
)
assert exit_code == 0
@@ -103,9 +97,7 @@ def test_word_timeout_cleanup_acceptance_rejects_non_timeout_failure(
lambda mode: _capability(),
)
- def fail_before_timeout(
- command: list[str], **kwargs: object
- ) -> NativeCommandResult:
+ def fail_before_timeout(command: list[str], **kwargs: object) -> NativeCommandResult:
raise ValidationError("Word COM automation failed before timeout.")
monkeypatch.setattr(script, "run_native_command", fail_before_timeout)
diff --git a/tests/unit/test_atomic.py b/tests/unit/test_atomic.py
index d280585..7128a87 100644
--- a/tests/unit/test_atomic.py
+++ b/tests/unit/test_atomic.py
@@ -76,9 +76,10 @@ def failing_fsync(fd: int) -> None:
monkeypatch.setattr(atomic.os, "fsync", failing_fsync)
- with pytest.raises(OSError, match="simulated fsync failure"), atomic_output(
- target, overwrite=True
- ) as temp:
+ with (
+ pytest.raises(OSError, match="simulated fsync failure"),
+ atomic_output(target, overwrite=True) as temp,
+ ):
temp.write_bytes(b"new")
assert target.read_bytes() == b"published"
diff --git a/tests/unit/test_docx_fidelity_corpus.py b/tests/unit/test_docx_fidelity_corpus.py
index 38e230e..8ae6981 100644
--- a/tests/unit/test_docx_fidelity_corpus.py
+++ b/tests/unit/test_docx_fidelity_corpus.py
@@ -68,9 +68,7 @@ def test_run_fidelity_corpus_keeps_report_paths_relative(
monkeypatch.setattr(
fidelity_corpus,
"run_fidelity_roundtrip_acceptance",
- lambda source, destination, mode, **kwargs: _accepted_evidence(
- source, destination, mode
- ),
+ lambda source, destination, mode, **kwargs: _accepted_evidence(source, destination, mode),
)
report = fidelity_corpus.run_fidelity_corpus(corpus, output, "libreoffice")
payload = report.to_dict()
diff --git a/tests/unit/test_docx_section_evidence.py b/tests/unit/test_docx_section_evidence.py
index 83ddbd5..8393216 100644
--- a/tests/unit/test_docx_section_evidence.py
+++ b/tests/unit/test_docx_section_evidence.py
@@ -136,9 +136,9 @@ def test_page_number_fingerprint_binds_document_order(tmp_path: Path) -> None:
chapter_separator="",
)
- assert page_number_properties_sha256(
- [first, second]
- ) != page_number_properties_sha256([second, first])
+ assert page_number_properties_sha256([first, second]) != page_number_properties_sha256(
+ [second, first]
+ )
def test_page_number_evidence_rejects_non_docx(tmp_path: Path) -> None:
diff --git a/tests/unit/test_docx_word_merge.py b/tests/unit/test_docx_word_merge.py
index 34111c5..d9a7998 100644
--- a/tests/unit/test_docx_word_merge.py
+++ b/tests/unit/test_docx_word_merge.py
@@ -59,9 +59,7 @@ def fake_run(command: list[str], *, timeout_seconds: int) -> NativeCommandResult
destination = Path(command[command.index("-Destination") + 1])
sources = json.loads(manifest.read_text(encoding="utf-8"))
captured["sources"] = sources
- captured["script"] = Path(command[command.index("-File") + 1]).read_text(
- encoding="utf-8"
- )
+ captured["script"] = Path(command[command.index("-File") + 1]).read_text(encoding="utf-8")
_write_identity_from_command(command)
merged = Document()
@@ -74,9 +72,7 @@ def fake_run(command: list[str], *, timeout_seconds: int) -> NativeCommandResult
cleanup_calls: list[tuple[Path, str]] = []
- def fake_cleanup(
- identity_file: Path, *, powershell: str
- ) -> WordProcessCleanupResult:
+ def fake_cleanup(identity_file: Path, *, powershell: str) -> WordProcessCleanupResult:
cleanup_calls.append((identity_file, powershell))
return _clean_process_result()
@@ -157,9 +153,7 @@ def failing_run(command: list[str], *, timeout_seconds: int) -> NativeCommandRes
_write_identity_from_command(command)
raise TimeoutError("simulated PowerShell timeout")
- def fake_cleanup(
- identity_file: Path, *, powershell: str
- ) -> WordProcessCleanupResult:
+ def fake_cleanup(identity_file: Path, *, powershell: str) -> WordProcessCleanupResult:
cleanup_calls.append(identity_file)
return WordProcessCleanupResult(
identity_present=True,
diff --git a/tests/unit/test_docx_word_merge_acceptance.py b/tests/unit/test_docx_word_merge_acceptance.py
index a05072a..8e9bf3f 100644
--- a/tests/unit/test_docx_word_merge_acceptance.py
+++ b/tests/unit/test_docx_word_merge_acceptance.py
@@ -67,9 +67,7 @@ def _synthetic_merge(sources: tuple[Path, ...], output: Path) -> None:
for index, source in enumerate(sources):
current = Document(str(source))
target_section = (
- merged.sections[0]
- if index == 0
- else merged.add_section(WD_SECTION.NEW_PAGE)
+ merged.sections[0] if index == 0 else merged.add_section(WD_SECTION.NEW_PAGE)
)
_copy_section_layout(current.sections[0], target_section)
for paragraph in current.paragraphs:
diff --git a/tests/unit/test_docx_word_merge_cleanup_failure.py b/tests/unit/test_docx_word_merge_cleanup_failure.py
index 1543d07..9766fd2 100644
--- a/tests/unit/test_docx_word_merge_cleanup_failure.py
+++ b/tests/unit/test_docx_word_merge_cleanup_failure.py
@@ -23,9 +23,7 @@ def test_word_native_merge_surfaces_cleanup_failure_over_original_timeout(
output = tmp_path / "merged.docx"
_write_docx(source)
- def failing_run(
- command: list[str], *, timeout_seconds: int
- ) -> NativeCommandResult:
+ def failing_run(command: list[str], *, timeout_seconds: int) -> NativeCommandResult:
identity = Path(command[command.index("-ProcessIdentityFile") + 1])
identity.write_text(
json.dumps(
diff --git a/tests/unit/test_docx_word_merge_page_number_acceptance.py b/tests/unit/test_docx_word_merge_page_number_acceptance.py
index 76b5594..20d59b7 100644
--- a/tests/unit/test_docx_word_merge_page_number_acceptance.py
+++ b/tests/unit/test_docx_word_merge_page_number_acceptance.py
@@ -28,9 +28,7 @@ def _inject_page_number_properties(path: Path, *, start: str, fmt: str) -> None:
if marker not in document_xml:
raise AssertionError("Expected section columns marker in generated DOCX fixture")
page_number = f''.encode()
- members["word/document.xml"] = document_xml.replace(
- marker, page_number + marker, 1
- )
+ members["word/document.xml"] = document_xml.replace(marker, page_number + marker, 1)
with ZipFile(path, "w", compression=ZIP_DEFLATED) as output:
for name, payload in members.items():
output.writestr(name, payload)
@@ -65,17 +63,13 @@ def _copy_section_layout(source: object, target: object) -> None:
target_story.is_linked_to_previous = source_story.is_linked_to_previous
-def _merge_without_page_number_properties(
- sources: tuple[Path, ...], output: Path
-) -> None:
+def _merge_without_page_number_properties(sources: tuple[Path, ...], output: Path) -> None:
merged = Document()
merged._body.clear_content()
for index, source in enumerate(sources):
current = Document(str(source))
target_section = (
- merged.sections[0]
- if index == 0
- else merged.add_section(WD_SECTION.NEW_PAGE)
+ merged.sections[0] if index == 0 else merged.add_section(WD_SECTION.NEW_PAGE)
)
_copy_section_layout(current.sections[0], target_section)
for paragraph in current.paragraphs:
@@ -137,10 +131,7 @@ def fake_merge(
evidence.expected_content.body_paragraphs_sha256
== evidence.output_content.body_paragraphs_sha256
)
- assert (
- evidence.expected_content.tables_sha256
- == evidence.output_content.tables_sha256
- )
+ assert evidence.expected_content.tables_sha256 == evidence.output_content.tables_sha256
assert (
evidence.expected_content.section_properties_sha256
== evidence.output_content.section_properties_sha256
diff --git a/tests/unit/test_lo_uno_acceptance_workflow.py b/tests/unit/test_lo_uno_acceptance_workflow.py
index b12e9a2..40b38e4 100644
--- a/tests/unit/test_lo_uno_acceptance_workflow.py
+++ b/tests/unit/test_lo_uno_acceptance_workflow.py
@@ -3,12 +3,7 @@
def _workflow_text() -> str:
repository_root = Path(__file__).resolve().parents[2]
- workflow = (
- repository_root
- / ".github"
- / "workflows"
- / "libreoffice-uno-acceptance.yml"
- )
+ workflow = repository_root / ".github" / "workflows" / "libreoffice-uno-acceptance.yml"
return workflow.read_text(encoding="utf-8")
diff --git a/tests/unit/test_ooxml_risk.py b/tests/unit/test_ooxml_risk.py
index ed54fc3..49f5e07 100644
--- a/tests/unit/test_ooxml_risk.py
+++ b/tests/unit/test_ooxml_risk.py
@@ -3,7 +3,6 @@
from docmergeforge.validation.ooxml import risky_docx_constructs
-
_CONTENT_TYPES = """
diff --git a/tests/unit/test_output_transaction_hardening.py b/tests/unit/test_output_transaction_hardening.py
index f358618..f05952a 100644
--- a/tests/unit/test_output_transaction_hardening.py
+++ b/tests/unit/test_output_transaction_hardening.py
@@ -34,9 +34,7 @@ def _entry(
"backup_name": backup_name,
"staged_size": len(staged_data) if staged_size is None else staged_size,
"staged_sha256": (
- hashlib.sha256(staged_data).hexdigest()
- if staged_sha256 is None
- else staged_sha256
+ hashlib.sha256(staged_data).hexdigest() if staged_sha256 is None else staged_sha256
),
}
diff --git a/tests/unit/test_scanner_exclusions.py b/tests/unit/test_scanner_exclusions.py
index 8cf186c..cc16e23 100644
--- a/tests/unit/test_scanner_exclusions.py
+++ b/tests/unit/test_scanner_exclusions.py
@@ -66,6 +66,9 @@ def test_recursive_iter_files_prunes_excluded_directory_before_descent(
source.mkdir()
excluded = source / "Master"
content = source / "Content"
+ content.mkdir()
+ (source / "root.txt").write_text("root", encoding="utf-8")
+ (content / "Part 1.docx").write_text("part", encoding="utf-8")
directory_names = ["Master", "Content"]
walk_calls: list[tuple[Path, bool]] = []
@@ -107,9 +110,7 @@ def test_non_recursive_iter_files_still_honors_excluded_root(tmp_path: Path) ->
included.write_text("one", encoding="utf-8")
excluded_file.write_text("two", encoding="utf-8")
- discovered = list(
- scanner.iter_files([source], recursive=False, exclude_roots=[excluded])
- )
+ discovered = list(scanner.iter_files([source], recursive=False, exclude_roots=[excluded]))
assert discovered == [included]
diff --git a/tests/unit/test_ui_resources.py b/tests/unit/test_ui_resources.py
index cc407c5..cc7906d 100644
--- a/tests/unit/test_ui_resources.py
+++ b/tests/unit/test_ui_resources.py
@@ -10,7 +10,7 @@
def test_desktop_support_links_match_canonical_project_values() -> None:
assert REPOSITORY_URL == "https://github.com/sanskarIN/DocMergeForge"
- assert DOCS_URL == f"{REPOSITORY_URL}/tree/main/docs"
+ assert f"{REPOSITORY_URL}/tree/main/docs" == DOCS_URL
assert BMC_URL == "https://buymeacoffee.com/sanskarIN"
assert X_URL == "https://x.com/x_sanskarIN"
assert BUSINESS_EMAIL == "sanskarin@outlook.in"