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
39 changes: 36 additions & 3 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,23 @@ from docx_editor import Document

### Opening Documents

#### `Document.open(path, author=None, force_recreate=False)`
#### `Document.open(path, author=None, force_recreate=False, workspace_dir=None)`

Open a Word document for editing.

**Parameters:**

- `path` (str | Path): Path to the .docx file
- `author` (str, optional): Author name for tracked changes. Defaults to system username.
- `force_recreate` (bool): If True, delete any existing workspace (stale or in-sync) before opening — any unsaved edits in the workspace are discarded. Use this to recover from `WorkspaceSyncError`. Defaults to False.
- `force_recreate` (bool): If True, delete any existing workspace (stale or in-sync) before opening — whatever XML it holds is discarded — and re-unpack from the current source. Use this to recover from `WorkspaceSyncError`. Defaults to False.
- `workspace_dir` (str | Path, optional): Base directory for the workspace. Overrides the `DOCX_EDITOR_WORKSPACE_DIR` environment variable and the platform cache default (see [Workspace location](#workspace-location)). A relative path resolves against the document's directory, so `workspace_dir=".docx"` keeps the workspace next to the file. Defaults to None.

**Returns:** Document instance ready for editing

**Raises:**

- `WorkspaceSyncError`: If the source `.docx` was modified since the workspace was created. Pass `force_recreate=True` to discard the stale workspace and re-unpack from the current source. The workspace is never deleted silently.
- `WorkspaceSyncError`: If the source `.docx` was modified since the workspace was created. Pass `force_recreate=True` to discard the stale workspace and re-unpack from the current source. The workspace is never deleted silently. The error message includes the workspace path.
- `WorkspaceError`: If the workspace directory cannot be created (e.g. the base is not writable), the home directory backing the default cache cannot be determined, or an existing workspace was unpacked from a different document. The message names the override to set.

**Example:**

Expand All @@ -33,6 +35,29 @@ doc = Document.open("contract.docx")
doc = Document.open("contract.docx", author="Legal Team")
```

#### Workspace location

When you open a document, its unpacked OOXML contents are stored in a workspace directory. By default this lives under the platform user cache, in a subfolder `docx-editor/<hash>` where `<hash>` is derived from 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`) |

To override the location:

- Set the `DOCX_EDITOR_WORKSPACE_DIR` environment variable to a base directory, or
- Pass `workspace_dir=` to `Document.open()` (takes precedence over the environment variable).

Both overrides are tilde-expanded, and an empty value counts as unset. A **relative** override resolves against the document's directory, so `workspace_dir=".docx"` reproduces the old next-to-file layout (handy for debugging). An **absolute** override is used as-is. (A relative `XDG_CACHE_HOME` / `%LOCALAPPDATA%` is ignored, per the XDG spec.)

Cleanup semantics are unchanged: the workspace persists until `close()` is called, and `close(cleanup=False)` preserves it for inspection. `close()` removes only the document's own workspace folder — the base directory is shared and is never deleted, so it is safe to point `workspace_dir` at a directory you also use for other things.

The workspace directory is created with owner-only permissions (`0o700`), since it holds the document's plaintext in a shared cache location. Use `doc.workspace_path` to locate it.

> **Note:** The default location moved from the old `.docx/<stem>/` folder next to the document to the platform cache. Workspaces created by older versions are no longer found and are simply ignored. Delete leftover `.docx/` folders, or pass `workspace_dir=".docx"` to keep using the old layout.

### Properties

#### `author`
Expand All @@ -51,6 +76,14 @@ Get the path to the source document.
print(doc.source_path) # Path("/path/to/contract.docx")
```

#### `workspace_path`

Get the path to this document's workspace folder. Since the workspace lives in the user cache by default, this is how you locate the unpacked XML — for example after `close(cleanup=False)`, or when a workspace was preserved because an exception was raised.

```python
print(doc.workspace_path) # Path("/home/you/.cache/docx-editor/0bebafb463a87cfa")
```

### Track Changes Methods

#### `paragraph_count()`
Expand Down
18 changes: 16 additions & 2 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,26 @@ with Document.open("contract.docx", author="Legal Team") as doc:
doc.save()
```

If the source `.docx` was modified outside this library since the workspace was created, `Document.open()` raises `WorkspaceSyncError` instead of silently discarding the workspace. Pass `force_recreate=True` to acknowledge the divergence and re-unpack from the current source:
If the source `.docx` was modified outside this library since the workspace was created, `Document.open()` raises `WorkspaceSyncError` instead of silently discarding the workspace. The error message includes the workspace path. Pass `force_recreate=True` to acknowledge the divergence and re-unpack from the current source:

```python
doc = Document.open("contract.docx", force_recreate=True)
```

## Workspace location

Opening a document unpacks it into a workspace directory. By default this lives under the platform user cache — `~/.cache/docx-editor/` on Linux (or `$XDG_CACHE_HOME`), `~/Library/Caches/docx-editor/` on macOS, `%LOCALAPPDATA%\docx-editor\` on Windows — in a subfolder named after a hash of the document's absolute path.

Override the base directory with the `DOCX_EDITOR_WORKSPACE_DIR` environment variable, or per-call with `workspace_dir=`:

```python
# Keep the workspace next to the document (relative path → resolved against
# the document's directory); useful for inspecting the unpacked XML.
doc = Document.open("contract.docx", workspace_dir=".docx")
```

The default location moved from the old `.docx/<stem>/` folder next to the document. Leftover `.docx/` folders from older versions are no longer used and can be deleted.

## Paragraph References

Tracked edit methods are scoped to a hash-anchored paragraph reference. Start by listing paragraphs:
Expand Down Expand Up @@ -203,7 +217,7 @@ print(f"Rejected {count} revisions")
doc.save() # Save to original path
doc.save("contract_v2.docx") # Save to a new path
doc.close() # Delete workspace
doc.close(cleanup=False) # Keep workspace for inspection
doc.close(cleanup=False) # Keep workspace (in the cache dir) for inspection
```

## Complete Example
Expand Down
43 changes: 35 additions & 8 deletions docx_editor/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,26 +83,43 @@ def open(
path: str | Path,
author: str | None = None,
force_recreate: bool = False,
workspace_dir: str | Path | None = None,
) -> "Document":
"""Open a Word document for editing.

Creates a workspace in .docx/{document_stem}/ alongside the document.
The workspace persists until close() is called.
Creates a workspace holding the document's unpacked contents. By
default the workspace lives under the platform user cache directory
(Linux: ``$XDG_CACHE_HOME`` or ``~/.cache``; macOS:
``~/Library/Caches``; Windows: ``%LOCALAPPDATA%``), in a subfolder
named ``docx-editor/<hash>`` where ``<hash>`` is derived from the
document's absolute path. The workspace persists until close() is
called.

Args:
path: Path to the .docx file
author: Author name for tracked changes (defaults to system username)
force_recreate: If True, delete any existing workspace (stale or
in-sync) before opening. Any unsaved edits in the workspace
are discarded. Use this to recover from WorkspaceSyncError.
in-sync) before opening, discarding whatever XML it holds, and
re-unpack from the current source. Use this to recover from
WorkspaceSyncError.
workspace_dir: Base directory for the workspace. Overrides the
DOCX_EDITOR_WORKSPACE_DIR environment variable and the platform
cache default. Tilde-expanded; an empty value counts as unset.
A relative path resolves against the document's directory, so
``workspace_dir=".docx"`` keeps the workspace next to the file
(handy for debugging).

Returns:
Document instance ready for editing

Raises:
WorkspaceError: If the workspace cannot be created (unwritable base,
undeterminable home directory) or an existing workspace was
unpacked from a different document.
WorkspaceSyncError: If the source document was modified since the
workspace was created. Pass force_recreate=True to discard the
stale workspace and re-unpack from the current source.
workspace was created. The message includes the workspace path.
Pass force_recreate=True to discard the stale workspace and
re-unpack from the current source.

Example:
doc = Document.open("contract.docx")
Expand All @@ -111,9 +128,9 @@ def open(
path = Path(path).resolve()

if force_recreate:
Workspace.delete(path)
Workspace.delete(path, workspace_dir=workspace_dir)

workspace = Workspace(path, author=author, create=True)
workspace = Workspace(path, author=author, create=True, workspace_dir=workspace_dir)
return cls(workspace)

@property
Expand All @@ -126,6 +143,16 @@ def source_path(self) -> Path:
"""Get the path to the source document."""
return self._workspace.source_path

@property
def workspace_path(self) -> Path:
"""Get the path to this document's workspace folder.

The workspace lives under the user cache by default, so this is the
only way to locate the unpacked XML — e.g. after close(cleanup=False)
or when a workspace is preserved because an exception was raised.
"""
return self._workspace.workspace_path

# ==================== Track Changes API ====================

def find_text(self, text: str, occurrence: int = 0):
Expand Down
Loading
Loading