Skip to content

Commit 1d5f0d0

Browse files
committed
changed Transaction interface and added documentation
1 parent 35379d7 commit 1d5f0d0

File tree

2 files changed

+76
-33
lines changed

2 files changed

+76
-33
lines changed

tests/fsutil/test_transaction.py

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -41,30 +41,34 @@ def _run_transaction_test(self, fail: bool):
4141
self.assertFalse(test_folder.exists())
4242
self.assertFalse(test_file_3.exists())
4343

44-
lock_file = test_root.parent / (test_root.filename + LOCK_EXT)
44+
temp_dir = FileObj("memory://temp")
45+
transaction = Transaction(test_root, temp_dir)
46+
47+
self.assertEqual(test_root, transaction.target_dir)
48+
49+
lock_file = transaction.lock_file
4550
self.assertFalse(lock_file.exists())
4651

47-
rollback_dir = FileObj("memory://rollback")
52+
rollback_dir = transaction.rollback_dir
4853
rollback_file = rollback_dir / ROLLBACK_FILE
4954
self.assertFalse(rollback_file.exists())
5055

5156
def change_test_file_1(rollback_cb: Callable):
5257
original_data = test_file_1.read()
5358
test_file_1.write("D-E-F")
54-
rollback_cb("replace_file", test_file_1.path, original_data)
59+
rollback_cb("replace_file", test_file_1.filename, original_data)
5560

5661
def create_test_file_2(rollback_cb: Callable):
5762
test_file_2.write("1-2-3")
58-
rollback_cb("delete_file", test_file_2.path, None)
63+
rollback_cb("delete_file", test_file_2.filename, None)
5964

6065
def create_test_folder(rollback_cb: Callable):
6166
test_folder.mkdir()
6267
test_file_3.write("4-5-6")
63-
rollback_cb("delete_dir", test_folder.path, None)
68+
rollback_cb("delete_dir", test_folder.filename, None)
6469

6570
try:
66-
with Transaction(test_root, rollback_dir,
67-
create_rollback_subdir=False) as rollback_cb:
71+
with transaction as rollback_cb:
6872
self.assertTrue(rollback_file.exists())
6973
self.assertTrue(lock_file.exists())
7074

@@ -77,9 +81,9 @@ def create_test_folder(rollback_cb: Callable):
7781
for line in rollback_data.split("\n")]
7882
self.assertEqual(
7983
[
80-
["replace_file", "/test/file-1.txt"],
81-
["delete_file", "/test/file-2.txt"],
82-
["delete_dir", "/test/folder"],
84+
["replace_file", "file-1.txt"],
85+
["delete_file", "file-2.txt"],
86+
["delete_dir", "folder"],
8387
[]
8488
],
8589
rollback_records
@@ -152,35 +156,33 @@ def test_it_raises_if_not_used_with_with(self):
152156
def test_deletes_lock(self):
153157
test_root = FileObj("memory://test")
154158
test_root.mkdir()
155-
rollback_dir = FileObj("memory://rollback")
156-
rollback_dir.mkdir()
157-
transaction = Transaction(test_root, rollback_dir,
158-
create_rollback_subdir=False)
159-
self.assertFalse(transaction._lock_file.exists())
159+
temp_dir = FileObj("memory://temp")
160+
temp_dir.mkdir()
161+
transaction = Transaction(test_root, temp_dir)
162+
self.assertFalse(transaction.lock_file.exists())
160163
with transaction:
161-
self.assertTrue(transaction._lock_file.exists())
162-
self.assertFalse(transaction._lock_file.exists())
164+
self.assertTrue(transaction.lock_file.exists())
165+
self.assertFalse(transaction.lock_file.exists())
163166

164167
def test_leaves_lock_behind_when_it_cannot_be_deleted(self):
165168
test_root = FileObj("memory://test")
166169
test_root.mkdir()
167-
rollback_dir = FileObj("memory://rollback")
168-
rollback_dir.mkdir()
169-
transaction = Transaction(test_root, rollback_dir,
170-
create_rollback_subdir=False)
170+
temp_dir = FileObj("memory://temp")
171+
temp_dir.mkdir()
172+
transaction = Transaction(test_root, temp_dir)
171173
delete_called = False
172174

173175
def _delete():
174176
nonlocal delete_called
175177
delete_called = True
176178
raise OSError("Bam!")
177179

178-
transaction._lock_file.delete = _delete
179-
self.assertFalse(transaction._lock_file.exists())
180+
transaction.lock_file.delete = _delete
181+
self.assertFalse(transaction.lock_file.exists())
180182
with transaction:
181-
self.assertTrue(transaction._lock_file.exists())
183+
self.assertTrue(transaction.lock_file.exists())
182184
self.assertEqual(True, delete_called)
183-
self.assertTrue(transaction._lock_file.exists())
185+
self.assertTrue(transaction.lock_file.exists())
184186

185187
# noinspection PyMethodMayBeStatic
186188
def test_it_raises_on_illegal_callback_calls(self):

zappend/fsutil/transaction.py

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,24 +33,66 @@ class Transaction:
3333
"""
3434
A filesystem transaction.
3535
36-
See https://github.com/zarr-developers/zarr-python/issues/247
36+
Its main motivation is implementing transactional Zarr dataset
37+
modifications, because this does not exist for Zarr yet (2024-01), see
38+
https://github.com/zarr-developers/zarr-python/issues/247.
39+
40+
The ``Transaction`` class is used to observe changes to a given target
41+
directory *target_dir*.
42+
43+
Changes must be explicitly registered using a "rollback callback"
44+
function that is provided as the result of using the
45+
transaction instance as a context manager:::
46+
47+
with Transaction(target_dir, temp_dir) as rollback_cb:
48+
# emit rollback actions here
49+
50+
The following actions are supported:
51+
52+
* ``rollback_cb("delete_dir", path)`` if a directory has been created.
53+
* ``rollback_cb("delete_file", path)`` if a file has been created.
54+
* ``rollback_cb("replace_file", path, original_data)`` if a directory has
55+
been changed.
56+
57+
Reported paths must be relative to *target_dir*. The empty path ``""``
58+
refers to *target_dir* itself.
59+
60+
:param target_dir: The target directory that is subject to this
61+
transaction. All paths emitted to the rollback callback must be
62+
relative to *target_dir*. The directory may or may not exist yet.
63+
:param temp_dir: Temporary directory in which a unique subdirectory
64+
will be created that will be used to collect
65+
rollback data during the transaction. The directory must exist.
3766
"""
3867

3968
def __init__(self,
4069
target_dir: FileObj,
41-
rollback_dir: FileObj,
42-
create_rollback_subdir: bool = True):
70+
temp_dir: FileObj):
71+
transaction_id = f"zappend-{uuid.uuid4()}"
72+
rollback_dir = temp_dir / transaction_id
4373
lock_file = target_dir.parent / (target_dir.filename + LOCK_EXT)
44-
transaction_id = str(uuid.uuid4())
45-
if create_rollback_subdir:
46-
rollback_dir = rollback_dir / transaction_id
4774
self._id = transaction_id
4875
self._rollback_dir = rollback_dir
4976
self._rollback_file = rollback_dir / ROLLBACK_FILE
5077
self._target_dir = target_dir
5178
self._lock_file = lock_file
5279
self._entered_ctx = False
5380

81+
@property
82+
def target_dir(self) -> FileObj:
83+
"""Target directory that is subject to this transaction."""
84+
return self._target_dir
85+
86+
@property
87+
def lock_file(self) -> FileObj:
88+
"""Temporary lock file used during the transaction."""
89+
return self._lock_file
90+
91+
@property
92+
def rollback_dir(self) -> FileObj:
93+
"""Temporary directory containing rollback data."""
94+
return self._rollback_dir
95+
5496
def __enter__(self):
5597
if self._entered_ctx:
5698
raise ValueError("Transaction instance cannot be used"
@@ -62,8 +104,7 @@ def __enter__(self):
62104
raise IOError(f"Target is locked: {lock_file.uri}")
63105
lock_file.write(self._rollback_dir.uri)
64106

65-
if not self._rollback_dir.exists():
66-
self._rollback_dir.mkdir()
107+
self._rollback_dir.mkdir()
67108
self._rollback_file.write("") # touch
68109

69110
return self._add_rollback_action

0 commit comments

Comments
 (0)