-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path_writer.py
More file actions
151 lines (115 loc) · 4.16 KB
/
Copy path_writer.py
File metadata and controls
151 lines (115 loc) · 4.16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
"""File writing logic for task mutations."""
from __future__ import annotations
import tempfile
from collections import defaultdict
from pathlib import Path
from taskmark._types import (
PendingChange,
TaskStore,
WriteResult,
)
def write_updates(store: TaskStore) -> WriteResult:
"""Write all pending changes to files.
Processes files independently. Within each file, processes
tasks bottom-up by line number to avoid index drift.
Each file is written atomically (temp file + rename).
Args:
store: TaskStore with pending changes from update()/batch_update()
Returns:
WriteResult with list of modified files and stats
"""
if not store._pending:
return WriteResult()
# Group pending changes by file
by_file = _group_by_file(store._pending)
result = WriteResult()
for file_path, changes in by_file.items():
# Get file info
file_info = store.files.get(file_path)
if file_info is None:
result.files_unchanged.append(file_path)
continue
# Write the file
modified, lines_changed = _write_file(file_path, file_info.lines, changes)
if modified:
result.files_modified.append(file_path)
result.lines_changed += lines_changed
result.tasks_updated += len(changes)
else:
result.files_unchanged.append(file_path)
# Clear pending changes
store._pending.clear()
return result
def _group_by_file(
pending: dict[tuple[Path, int], PendingChange],
) -> dict[Path, list[tuple[int, PendingChange]]]:
"""Group pending changes by file path.
Returns:
Dict mapping file path to list of (line_number, pending_change) tuples
"""
by_file: dict[Path, list[tuple[int, PendingChange]]] = defaultdict(list)
for (file_path, line_num), change in pending.items():
by_file[file_path].append((line_num, change))
return dict(by_file)
def _write_file(
file_path: Path,
original_lines: list[str],
changes: list[tuple[int, PendingChange]],
) -> tuple[bool, int]:
"""Write changes to a single file atomically.
Args:
file_path: Path to write to
original_lines: Original file lines (without newlines)
changes: List of (line_number, pending_change) tuples
Returns:
(was_modified, lines_changed_count)
"""
# Sort by line number descending (bottom-up)
sorted_changes = sorted(changes, key=lambda x: x[0], reverse=True)
# Copy lines (lines don't have newlines)
lines = list(original_lines)
lines_changed = 0
for line_num, change in sorted_changes:
# Line numbers are 1-indexed
idx = line_num - 1
if idx < 0 or idx >= len(lines):
continue
# Generate new line content (without newline)
new_line = change.task.to_markdown_line()
# Replace the line
if lines[idx] != new_line:
lines[idx] = new_line
lines_changed += 1
# Insert new recurring task if present
if change.new_recurring_task:
new_recurring_line = change.new_recurring_task.to_markdown_line()
# Insert after the current line
lines.insert(idx + 1, new_recurring_line)
lines_changed += 1
if lines_changed == 0:
return False, 0
# Atomic write: temp file + rename
# Join lines with newlines to reconstruct the file
_atomic_write(file_path, lines)
return True, lines_changed
def _atomic_write(file_path: Path, lines: list[str]) -> None:
"""Write lines to file atomically using temp file + rename.
Args:
file_path: Target file path
lines: Lines without newlines (will be joined with \\n)
"""
# Write to temp file in same directory
parent = file_path.parent
with tempfile.NamedTemporaryFile(
mode="w",
dir=parent,
delete=False,
suffix=".tmp",
encoding="utf-8",
) as f:
# Join lines with newlines to reconstruct file content
content = "\n".join(lines)
f.write(content)
temp_path = Path(f.name)
# Rename temp to target (atomic on POSIX)
temp_path.replace(file_path)