Skip to content

Commit bc04d7d

Browse files
authored
python to_be_file (#366)
2 parents c142c6c + c97896d commit bc04d7d

File tree

7 files changed

+202
-14
lines changed

7 files changed

+202
-14
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from selfie_lib.WriteTracker import (
2+
ToBeFileWriteTracker,
3+
ToBeFileLazyBytes,
4+
recordCall,
5+
TypedPath,
6+
SnapshotFileLayout,
7+
)
8+
from pytest_selfie import FSImplementation
9+
from pathlib import Path
10+
import os
11+
12+
13+
def test_to_be_file():
14+
layout = SnapshotFileLayout(FSImplementation())
15+
tracker = ToBeFileWriteTracker()
16+
17+
# Record the current call stack.
18+
call_stack = recordCall(callerFileOnly=True)
19+
20+
# Create a TypedPath object for the file path
21+
file_path = TypedPath(os.path.abspath(Path("test.jpg")))
22+
23+
# Write byte data to disk using the tracker.
24+
tracker.writeToDisk(file_path, b"some byte data", call_stack, layout)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
from .SelfieSettingsAPI import SelfieSettingsAPI as SelfieSettingsAPI
2+
from .plugin import FSImplementation as FSImplementation

python/pytest-selfie/pytest_selfie/plugin.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typing import ByteString, DefaultDict, List, Optional, Iterator
55

66
from selfie_lib.Atomic import AtomicReference
7+
from selfie_lib.WriteTracker import ToBeFileWriteTracker
78
from .SelfieSettingsAPI import SelfieSettingsAPI
89
from selfie_lib import (
910
_clearSelfieSystem,
@@ -149,7 +150,7 @@ def __init__(self, settings: SelfieSettingsAPI):
149150
self._layout = PytestSnapshotFileLayout(self.__fs, settings)
150151
self.__comment_tracker = CommentTracker()
151152
self.__inline_write_tracker = InlineWriteTracker()
152-
# self.__toBeFileWriteTracker = ToBeFileWriteTracker() #TODO
153+
self.__toBeFileWriteTracker = ToBeFileWriteTracker()
153154

154155
self.__progress_per_file: DefaultDict[TypedPath, SnapshotFileProgress] = (
155156
_keydefaultdict(lambda key: SnapshotFileProgress(self, key)) # type: ignore
@@ -247,7 +248,8 @@ def write_inline(self, literal_value: LiteralValue, call: CallStack):
247248
def write_to_be_file(
248249
self, path: TypedPath, data: "ByteString", call: CallStack
249250
) -> None:
250-
raise NotImplementedError
251+
# Directly write to disk using ToBeFileWriteTracker
252+
self.__toBeFileWriteTracker.writeToDisk(path, data, call, self._layout)
251253

252254

253255
class DiskStoragePytest(DiskStorage):

python/selfie-lib/selfie_lib/CacheSelfie.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from .Literals import LiteralValue, LiteralString, TodoStub
55
from .SnapshotSystem import DiskStorage
66
from .RoundTrip import Roundtrip
7+
import base64
78

89
T = TypeVar("T")
910

@@ -75,3 +76,111 @@ def _to_be_impl(self, snapshot: Optional[str]) -> T:
7576
raise Exception("Can't call `to_be_todo` in readonly mode!")
7677
else:
7778
return self.roundtrip.parse(snapshot)
79+
80+
81+
class CacheSelfieBinary(Generic[T]):
82+
def __init__(self, disk: DiskStorage, roundtrip: Roundtrip[T, bytes], generator):
83+
self.disk: DiskStorage = disk
84+
self.roundtrip: Roundtrip[T, bytes] = roundtrip
85+
self.generator = generator
86+
87+
def to_match_disk(self, sub: str = "") -> T:
88+
return self._to_match_disk_impl(sub, False)
89+
90+
def to_match_disk_TODO(self, sub: str = "") -> T:
91+
return self._to_match_disk_impl(sub, True)
92+
93+
def _to_match_disk_impl(self, sub: str, is_todo: bool) -> T:
94+
from .Selfie import _selfieSystem
95+
96+
system = _selfieSystem()
97+
call = recordCall(False)
98+
99+
if system.mode.can_write(is_todo, call, system):
100+
actual = self.generator()
101+
serialized_data = self.roundtrip.serialize(actual)
102+
self.disk.write_disk(Snapshot.of(serialized_data), sub, call)
103+
104+
if is_todo:
105+
system.write_inline(TodoStub.to_match_disk.create_literal(), call)
106+
107+
return actual
108+
else:
109+
if is_todo:
110+
raise Exception("Can't call `to_match_disk_TODO` in read-only mode!")
111+
else:
112+
snapshot = self.disk.read_disk(sub, call)
113+
114+
if snapshot is None:
115+
raise Exception(system.mode.msg_snapshot_not_found())
116+
117+
if snapshot.subject.is_binary or len(snapshot.facets) > 0:
118+
raise Exception(
119+
"Expected a binary subject with no facets, got {}".format(
120+
snapshot
121+
)
122+
)
123+
124+
return self.roundtrip.parse(snapshot.subject.value_binary())
125+
126+
def to_be_file_TODO(self, subpath: str) -> T:
127+
return self._to_be_file_impl(subpath, True)
128+
129+
def to_be_file(self, subpath: str) -> T:
130+
return self._to_be_file_impl(subpath, False)
131+
132+
def _to_be_file_impl(self, subpath: str, is_todo: bool) -> T:
133+
from .Selfie import _selfieSystem
134+
135+
system = _selfieSystem()
136+
call = recordCall(False)
137+
writable = system.mode.can_write(is_todo, call, system)
138+
139+
if writable:
140+
actual = self.generator()
141+
142+
if is_todo:
143+
system.write_inline(TodoStub.to_be_file.create_literal(), call)
144+
145+
with open(subpath, "wb") as file:
146+
file.write(self.roundtrip.serialize(actual))
147+
148+
return actual
149+
else:
150+
if is_todo:
151+
raise Exception("Can't call `toBeFile_TODO` in read-only mode!")
152+
else:
153+
with open(subpath, "rb") as file:
154+
serialized_data = file.read()
155+
return self.roundtrip.parse(serialized_data)
156+
157+
def to_be_base64_TODO(self, unused_arg: Optional[Any] = None) -> T:
158+
return self._to_be_base64_impl(None)
159+
160+
def to_be_base64(self, snapshot: str) -> T:
161+
return self._to_be_base64_impl(snapshot)
162+
163+
def _to_be_base64_impl(self, snapshot: Optional[str]) -> T:
164+
from .Selfie import _selfieSystem
165+
166+
system = _selfieSystem()
167+
call = recordCall(False)
168+
writable = system.mode.can_write(snapshot is None, call, system)
169+
170+
if writable:
171+
actual = self.generator()
172+
base64_data = base64.b64encode(self.roundtrip.serialize(actual)).decode(
173+
"utf-8"
174+
)
175+
literal_string_formatter = LiteralString()
176+
system.write_inline(
177+
LiteralValue(snapshot, base64_data, literal_string_formatter),
178+
call,
179+
)
180+
return actual
181+
else:
182+
if snapshot is None:
183+
raise Exception("Can't call `toBe_TODO` in read-only mode!")
184+
else:
185+
decoded_data = base64.b64decode(snapshot.encode("utf-8"))
186+
return self.roundtrip.parse(decoded_data)

python/selfie-lib/selfie_lib/FS.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,5 @@ def file_write_binary(self, typed_path: TypedPath, content: bytes):
3333
Path(typed_path.absolute_path).write_bytes(content)
3434

3535
@abstractmethod
36-
def assert_failed(self, message: str, expected=None, actual=None) -> Exception:
36+
def assert_failed(self, message, expected=None, actual=None) -> Exception:
3737
pass

python/selfie-lib/selfie_lib/Selfie.py

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import TypeVar, Any, Protocol, Union, overload
1+
from typing import Any, TypeVar, Optional, Protocol, Union, overload
22
from .SelfieImplementations import ReprSelfie, StringSelfie
33
from .SnapshotSystem import _selfieSystem
44
from .Snapshot import Snapshot
@@ -39,15 +39,25 @@ def expect_selfie(
3939
return ReprSelfie(actual)
4040

4141

42-
def cache_selfie_string(to_cache: Cacheable[str]) -> CacheSelfie[str]:
43-
"""Create a CacheSelfie instance for caching strings with identity transformation."""
44-
identity_roundtrip = Roundtrip.identity()
45-
return cache_selfie_generic(identity_roundtrip, to_cache)
42+
@overload
43+
def cache_selfie(to_cache: Cacheable[str]) -> CacheSelfie[str]: ...
4644

4745

48-
def cache_selfie_generic(
49-
roundtrip: Roundtrip[T, str], to_cache: Cacheable[T]
50-
) -> CacheSelfie[T]:
51-
"""Create a CacheSelfie instance for caching generic objects with specified roundtrip."""
52-
deferred_disk_storage = _selfieSystem().disk_thread_local()
53-
return CacheSelfie(deferred_disk_storage, roundtrip, to_cache)
46+
@overload
47+
def cache_selfie(
48+
to_cache: Cacheable[T], roundtrip: Roundtrip[T, str]
49+
) -> CacheSelfie[T]: ...
50+
51+
52+
def cache_selfie(
53+
to_cache: Union[Cacheable[str], Cacheable[T]],
54+
roundtrip: Optional[Roundtrip[T, str]] = None,
55+
) -> Union[CacheSelfie[str], CacheSelfie[T]]:
56+
if roundtrip is None:
57+
# the cacheable better be a string!
58+
return cache_selfie(to_cache, Roundtrip.identity()) # type: ignore
59+
elif isinstance(roundtrip, Roundtrip) and to_cache is not None:
60+
deferred_disk_storage = _selfieSystem().disk_thread_local()
61+
return CacheSelfie(deferred_disk_storage, roundtrip, to_cache)
62+
else:
63+
raise TypeError("Invalid arguments provided to cache_selfie")

python/selfie-lib/selfie_lib/WriteTracker.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,3 +239,45 @@ def persist_writes(self, layout: SnapshotFileLayout):
239239

240240
# Final write to disk for the last file processed
241241
layout.fs.file_write(current_file, content.as_string)
242+
243+
244+
class ToBeFileLazyBytes:
245+
def __init__(self, location: TypedPath, layout: SnapshotFileLayout, data: bytes):
246+
self.location = location
247+
self.layout = layout
248+
self.data = data
249+
250+
def writeToDisk(self) -> None:
251+
if self.data is None:
252+
raise Exception("Data has already been written to disk")
253+
self.layout.fs.file_write_binary(self.location, self.data)
254+
self.data = None # Allow garbage collection
255+
256+
def readData(self):
257+
if self.data is not None:
258+
return self.data
259+
return self.layout.fs.file_read_binary(self.location)
260+
261+
def __eq__(self, other):
262+
if not isinstance(other, ToBeFileLazyBytes):
263+
return False
264+
return self.readData() == other.readData()
265+
266+
def __hash__(self):
267+
return hash(self.readData())
268+
269+
270+
class ToBeFileWriteTracker(WriteTracker[TypedPath, ToBeFileLazyBytes]):
271+
def __init__(self):
272+
super().__init__()
273+
274+
def writeToDisk(
275+
self,
276+
key: TypedPath,
277+
snapshot: bytes,
278+
call: CallStack,
279+
layout: SnapshotFileLayout,
280+
) -> None:
281+
lazyBytes = ToBeFileLazyBytes(key, layout, snapshot)
282+
self.recordInternal(key, lazyBytes, call, layout)
283+
lazyBytes.writeToDisk()

0 commit comments

Comments
 (0)