Skip to content

refactor(editor): share atomic PDF write via app/pdf_io (R3) - #158

Merged
nelsonduarte merged 3 commits into
mainfrom
refactor/editor-atomic-write
Aug 11, 2026
Merged

refactor(editor): share atomic PDF write via app/pdf_io (R3)#158
nelsonduarte merged 3 commits into
mainfrom
refactor/editor-atomic-write

Conversation

@nelsonduarte

Copy link
Copy Markdown
Owner

R3 — Partilha da escrita atomica de PDF

Terceiro incremento (R3) do plano de modularidade do editor. Elimina a duplicacao da logica de escrita atomica de PDF que existia entre TabEditar._run (editor) e BasePage._atomic_pdf_write.

O que muda

  • Extrai o helper partilhado para o novo modulo app/pdf_io.py — modulo puro/baixo-nivel, sem dependencias de Qt e sem ciclos de import.
  • BasePage._atomic_pdf_write e TabEditar._run passam ambos a delegar em app/pdf_io.py.
  • Parametro opt-in close_writer=True preserva a ordem exigida pelo editor no Windows: save -> close -> replace (o lock de ficheiro do Windows exige fechar o writer antes do os.replace).

Decisao de design

  • Escolheu-se um helper partilhado em vez de heranca a partir de BasePage, que arrastaria dependencias de UI para o editor. O modulo fica puro e reutilizavel.

Comportamento

  • ZERO alteracao de comportamento: save plano e AES-256 preservados; destino nao corrompido em caso de falha (escrita para temporario + replace atomico).

Testes

  • +9 testes, incluindo round-trip AES-256 e verificacao portavel da ordem close-before-replace.
  • Suite completa: 585 passed / 0 failed.
  • Aprovado por revisao adversarial, reproduzida em Windows real.

Notas

  • _apply_forms fica para um incremento futuro.
  • Depende de R1/R2 (ja em main).

🤖 Generated with Claude Code

nelsonduarte and others added 2 commits August 11, 2026 13:17
Extract BasePage._atomic_pdf_write / _check_not_same_path into a pure
low-level app/pdf_io module and route both BasePage (all tools) and
TabEditar._run through it, removing the duplicated mkstemp + doc.save +
os.replace block in the editor's save path.

- app/pdf_io.py: new pure module (stdlib + lazy app.i18n import); adds an
  opt-in close_writer flag that preserves the editor's original
  save -> close -> replace ordering, needed on Windows when the output
  overwrites the same file the document was opened from.
- app/base.py: _atomic_pdf_write / _check_not_same_path become thin
  delegating staticmethods (signatures unchanged, so the ~20 call sites
  and the regression tests are untouched); drop the now-unused
  contextlib import.
- app/editor/tab.py: _run builds save_opts via dict(...) and calls the
  shared atomic_pdf_write with close_writer=True. Encryption behaviour
  (AES-256, owner==user, permissions) is byte-for-byte identical.

Behaviour is unchanged: plain and AES-256 save, temp cleanup on error,
the outer show_error path, and the close ordering all match. _apply_forms
keeps its own pypdf write for now (a follow-up increment).

Tests: retarget the os.replace monkeypatch to app.pdf_io (the code moved
there); add tests/test_editor_atomic_write.py covering plain + AES-256
round-trip, wrong-password rejection, close_writer ordering, verbatim
save_opts forwarding and the same-source guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The existing close_writer tests only inspect doc.is_closed after
atomic_pdf_write returns, so they cannot catch a regression that moved
writer.close() to AFTER os.replace: the doc ends up closed either way.
That reorder is load-bearing on Windows (the editor overwrites the file
it has open, so the handle must be released before the rename or
os.replace raises a sharing violation) yet silent on POSIX CI.

Pin the ordering portably by monkeypatching the os.replace app.pdf_io
calls with a spy that records doc.is_closed at the instant the rename
fires, then delegates to the real replace. One test asserts the fitz
handle is already closed at that instant (close_writer=True), the
inverse asserts it is still open under the BasePage default.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread tests/test_editor_atomic_write.py Fixed
Rename the module-level QApplication holder from _app to _unused_app in
tests/test_editor_atomic_write.py, matching the existing repo convention
(tests/test_encrypted_pdf_tools.py). The variable only exists to keep a
live QApplication for the Qt-backed lazy i18n import; the underscore-
unused name preserves that side effect while resolving CodeQL alert #304
(Unused global variable).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying pdfapps with  Cloudflare Pages  Cloudflare Pages

Latest commit: 59d3bbc
Status: ✅  Deploy successful!
Preview URL: https://7d9674f2.pdfapps.pages.dev
Branch Preview URL: https://refactor-editor-atomic-write.pdfapps.pages.dev

View logs

@nelsonduarte
nelsonduarte merged commit 3d9b0f8 into main Aug 11, 2026
3 checks passed
@nelsonduarte
nelsonduarte deleted the refactor/editor-atomic-write branch August 11, 2026 13:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants