Skip to content

feat: move workspace to platform cache directory (#12) - #29

Merged
pablospe merged 3 commits into
mainfrom
task/d8f559ed-Move-workspace-to-cache-direct
Jul 13, 2026
Merged

pablospe merged 3 commits into
mainfrom
task/d8f559ed-Move-workspace-to-cache-direct

Conversation

@pablospe

@pablospe pablospe commented Jul 13, 2026 •

Copy link
Copy Markdown
Owner

Summary

The per-document workspace previously lived in a .docx/<stem>/ folder next to the source file, cluttering user directories and risking collisions between same-named documents.

It now lives under the platform user cache, in docx-editor/<hash>/ where <hash> is the sha256 of the document's absolute path:

Platform Default base directory
Linux $XDG_CACHE_HOME/docx-editor (falls back to ~/.cache/docx-editor)
macOS ~/Library/Caches/docx-editor
Windows %LOCALAPPDATA%\docx-editor (falls back to ~\AppData\Local\docx-editor)

Overrides

The base directory can be overridden, in precedence order:

  1. the workspace_dir argument to Document.open() / Workspace()
  2. the DOCX_EDITOR_WORKSPACE_DIR environment variable
  3. the platform default

A relative override resolves against the source document's directory, so workspace_dir=".docx" reproduces the old next-to-file layout for debugging. An absolute override is used as-is.

doc = Document.open("contract.docx", workspace_dir=".docx")  # old layout, for inspecting XML

Implementation notes

  • Path resolution is centralized in Workspace._resolve_workspace_path(), replacing the duplicated parent / WORKSPACE_DIR / stem logic in __init__, exists(), and delete(). The WORKSPACE_DIR constant is gone.
  • Hash-based subdirs are keyed on the resolved absolute path, so distinct documents never collide and the old duplicate-stem / long-path issues go away.
  • Base-directory cleanup now suppresses OSError: the base is shared across documents and processes, so the check-then-rmdir can race. It only ever removes a verified-empty directory.
  • WorkspaceSyncError messages include the workspace path, which matters more now that the workspace isn't next to the file.

Breaking change

No auto-migration. Workspaces created by older versions are no longer found, and any unsaved edits still in them are ignored. Leftover .docx/ folders can be deleted, or pass workspace_dir=".docx" to keep using the old layout.

Testing

  • Full suite: 630 passed, 8 skipped
  • ruff check and ruff format --check: clean
  • New TestDefaultCacheDir (platform matrix) and TestWorkspaceLocation (default cache / env var / param override / relative override / distinct-source isolation / sync-error message) coverage
  • An autouse conftest fixture points DOCX_EDITOR_WORKSPACE_DIR at a throwaway temp dir, so tests never touch the real user cache

Closes #12

Summary by CodeRabbit

  • New Features

    • Added support for choosing a custom workspace location when opening documents.
    • Added environment-variable configuration for workspace storage.
    • Workspaces now use platform-appropriate cache directories by default.
  • Bug Fixes

    • Error messages now include the affected workspace path.
    • Improved workspace cleanup and recreation behavior.
  • Documentation

    • Updated API, quickstart, and usage guidance to explain workspace locations, cleanup, and troubleshooting.

The per-document workspace previously lived in a .docx/<stem>/ folder next
to the source file, cluttering user directories and risking collisions
between same-named documents.

It now lives under the platform user cache, in docx-editor/<hash>/ where
<hash> is sha256 of the document's absolute path:

  Linux    $XDG_CACHE_HOME/docx-editor (fallback ~/.cache/docx-editor)
  macOS    ~/Library/Caches/docx-editor
  Windows  %LOCALAPPDATA%\docx-editor (fallback ~/AppData/Local)

The base directory can be overridden, in precedence order, by the
workspace_dir argument to Document.open()/Workspace(), the
DOCX_EDITOR_WORKSPACE_DIR environment variable, or the platform default.
A relative override resolves against the source document's directory, so
workspace_dir=".docx" reproduces the old next-to-file layout for debugging.

Path resolution is centralized in Workspace._resolve_workspace_path(),
replacing the duplicated logic in __init__, exists(), and delete(). Base
directory cleanup suppresses OSError, since the base is now shared across
documents and processes.

No auto-migration: workspaces created by older versions are not found, and
leftover .docx/ folders can be deleted.
@coderabbitai

coderabbitai Bot commented Jul 13, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@pablospe, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 54 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 97f49bf4-157a-4978-9c9b-03fe4de5c513

📥 Commits

Reviewing files that changed from the base of the PR and between 1c39d8f and bd0c9eb.

📒 Files selected for processing (5)
  • docs/api.md
  • docx_editor/document.py
  • docx_editor/workspace.py
  • openspec/project.md
  • tests/test_workspace.py
📝 Walkthrough

Walkthrough

Document.open() now accepts workspace_dir, while workspace storage resolves to configurable, hashed cache paths with updated cleanup behavior. Tests cover platform defaults and overrides, and documentation describes the new locations, error messages, and lifecycle semantics.

Changes

Workspace location and API integration

Layer / File(s) Summary
Workspace path resolution and lifecycle
docx_editor/workspace.py, tests/conftest.py, tests/test_workspace.py
Workspace paths now use platform cache defaults, explicit or environment overrides, hashed document subdirectories, and shared-base cleanup, with expanded test coverage.
Document.open workspace override
docx_editor/document.py, tests/test_document.py, docs/api.md
Document.open() accepts and propagates workspace_dir, including during forced recreation, with updated API documentation and tests.
Workspace behavior documentation
docs/quickstart.md, skills/docx/SKILL.md
Guidance now describes workspace paths in errors, preserved workspaces, cache inspection, cleanup, and parallel-editing behavior.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant Document.open
  participant Workspace
  participant Filesystem
  Caller->>Document.open: provide document path and workspace_dir
  Document.open->>Workspace: resolve workspace and handle force_recreate
  Workspace->>Filesystem: create or delete hashed workspace
  Workspace-->>Document.open: workspace state
  Document.open-->>Caller: opened Document
Loading

Possibly related PRs

  • pablospe/docx-editor#3: Updates related documentation for preserved cache workspaces and workspace paths in error output.

Poem

A bunny hops through cache-land bright,
Hashing workspaces just right.
workspace_dir shows where to go,
Errors point to paths below.
Close or inspect— the choice is clear,
Fresh docs and tests now cheer! 🐇

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning Issue #12 asks for a plugin version bump, but the PR does not change .claude-plugin/plugin.json. Add the requested version bump in .claude-plugin/plugin.json from 0.1.1 to 0.2.0.
Out of Scope Changes check ⚠️ Warning The PR focuses on workspace-path refactors, docs, and tests, which are unrelated to the linked version-bump request. Split the workspace refactor into a separate PR, or add only the requested version bump here.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: moving workspaces to platform cache directories.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch task/d8f559ed-Move-workspace-to-cache-direct

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 13, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.16393% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.5%. Comparing base (72c5479) to head (bd0c9eb).

Files with missing lines Patch % Lines
docx_editor/workspace.py 91.0% 4 Missing and 1 partial ⚠️
docx_editor/document.py 80.0% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##            main     #29     +/-   ##
=======================================
- Coverage   94.6%   94.5%   -0.1%     
=======================================
  Files         10      10             
  Lines       2526    2568     +42     
  Branches     565     572      +7     
=======================================
+ Hits        2391    2429     +38     
- Misses        36      41      +5     
+ Partials      99      98      -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

pablospe added 2 commits July 13, 2026 15:59
Follow-up to the cache migration, from a multi-reviewer pass.

Crash: the workspace dir was keyed by sha256(str(path).encode()), but
str(Path) carries undecodable filename bytes as surrogates, which strict
UTF-8 rejects. Opening e.g. "café.docx" (latin-1 on disk) raised
UnicodeEncodeError. Hash via os.fsencode(os.path.normcase(path)) instead;
this is byte-identical for all decodable paths, so no workspace moves.
normcase also folds case on Windows, where one file could otherwise map to
two workspaces.

Data loss: close()/delete() removed the base directory when it was left
empty. That was safe when the base was the library's own .docx/ folder, but
the base is now the shared cache root -- or whatever directory the caller
passed via workspace_dir=/DOCX_EDITOR_WORKSPACE_DIR. The library was
deleting a directory it did not create. It now removes only the document's
own workspace folder. Two tests asserted the old behavior and are rewritten.

Path resolution: overrides are now tilde-expanded (DOCX_EDITOR_WORKSPACE_DIR
=~/ws created a literal "~" directory next to each document), an empty or
whitespace override counts as unset and falls through to the next precedence
level (workspace_dir="" silently rebased onto the document's own directory),
and a relative XDG_CACHE_HOME/%LOCALAPPDATA% is ignored per the XDG spec
rather than scattering caches beside every document.

Permissions: the workspace holds the document's plaintext in a shared,
predictably-named cache, so it and its base are created 0o700 instead of
inheriting a 0o775 umask default. mkdir(parents=True, mode=) applies the
mode to the leaf only, so the base is created explicitly.

Provenance: a workspace whose meta.json names a different source document is
now refused instead of silently adopted -- adopting it would let a later
save() pack that other document's XML over this one's source file.

Errors: filesystem failures at the new cache boundary (unwritable base, no
resolvable home) surfaced as raw PermissionError/RuntimeError out of
Document.open(). They are now WorkspaceError, naming the override to set.

Also: add a public Document.workspace_path -- the docs told users to inspect
the preserved workspace but gave them no way to locate it now that it is an
opaque hash under the cache. Drop the write-only self.workspace_dir, hoist
the function-local shutil import, and correct the "unsaved edits in the
workspace" wording (edits reach the workspace only on save()).
Found by a second adversarial pass over the previous commit.

The permissions test guarded itself with skipif(os.getuid() == 0). A skipif
condition is evaluated at collection time on every platform, and os.getuid
does not exist on Windows, so the whole test module would die on import there
-- taking with it the two tests that exist specifically to cover the Windows
cache dir. Guard on os.name first; `or` short-circuits before os.geteuid.

The provenance check compared the raw source path while the workspace key is
normcase-folded. On a case-insensitive filesystem two spellings map to one
workspace directory, which the check would then reject as "a different
document" -- and the message tells the user to pass force_recreate=True,
which would rmtree the workspace. Fold both sides the same way.

Stripping the override with str(override).strip() also stripped Path inputs,
silently redirecting a path that legitimately ends in whitespace to a
different directory. Strip str overrides (where stray whitespace is config
noise) and take a Path verbatim.

Also document the WorkspaceError exits that the hardening added to
Document.open(), and drop the literal ~ from the workspace_path example --
the value is always expanded.
@pablospe
pablospe merged commit 978c295 into main Jul 13, 2026
9 of 10 checks passed
@pablospe
pablospe deleted the task/d8f559ed-Move-workspace-to-cache-direct branch July 13, 2026 19:50
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.

1 participant