Skip to content
This repository was archived by the owner on Apr 4, 2024. It is now read-only.

Commit eb3e99c

Browse files
authored
SnapshotReader - Completed (#48)
2 parents 7f3ce67 + 639f36d commit eb3e99c

7 files changed

+292
-48
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
class ConvertToWindowsNewlines:
2+
def __init__(self, sink):
3+
self.sink = sink
4+
5+
def append(self, value, start_index=None, end_index=None):
6+
# If value is a single character
7+
if isinstance(value, str) and len(value) == 1:
8+
if value != "\n":
9+
self.sink.write(value)
10+
else:
11+
self.sink.write("\r\n")
12+
# If value is a CharSequence (in Python, a str)
13+
elif isinstance(value, str):
14+
# If start_index and end_index are provided, use the slice of the string
15+
if start_index is not None and end_index is not None:
16+
value_to_append = value[start_index:end_index]
17+
else:
18+
value_to_append = value
19+
self.sink.write(value_to_append.replace("\n", "\r\n"))
20+
return self
+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
from .SnapshotValue import SnapshotValue
2+
from collections import OrderedDict
3+
4+
5+
class Snapshot:
6+
def __init__(self, subject, facet_data):
7+
self._subject = subject
8+
self._facet_data = facet_data
9+
10+
@property
11+
def facets(self):
12+
return OrderedDict(sorted(self._facet_data.items()))
13+
14+
def __eq__(self, other):
15+
if not isinstance(other, Snapshot):
16+
return NotImplemented
17+
return self._subject == other._subject and self._facet_data == other._facet_data
18+
19+
def __hash__(self):
20+
return hash((self._subject, frozenset(self._facet_data.items())))
21+
22+
def plus_facet(self, key, value):
23+
if isinstance(value, bytes):
24+
value = SnapshotValue.of(value)
25+
elif isinstance(value, str):
26+
value = SnapshotValue.of(value)
27+
return self._plus_facet(key, value)
28+
29+
def _plus_facet(self, key, value):
30+
if not key:
31+
raise ValueError("The empty string is reserved for the subject.")
32+
facet_data = dict(self._facet_data)
33+
facet_data[self._unix_newlines(key)] = value
34+
return Snapshot(self._subject, facet_data)
35+
36+
def plus_or_replace(self, key, value):
37+
if not key:
38+
return Snapshot(value, self._facet_data)
39+
facet_data = dict(self._facet_data)
40+
facet_data[self._unix_newlines(key)] = value
41+
return Snapshot(self._subject, facet_data)
42+
43+
def subject_or_facet_maybe(self, key):
44+
if not key:
45+
return self._subject
46+
return self._facet_data.get(key)
47+
48+
def subject_or_facet(self, key):
49+
value = self.subject_or_facet_maybe(key)
50+
if value is None:
51+
raise KeyError(f"'{key}' not found in {list(self._facet_data.keys())}")
52+
return value
53+
54+
def all_entries(self):
55+
entries = [("", self._subject)]
56+
entries.extend(self._facet_data.items())
57+
return entries
58+
59+
def __bytes__(self):
60+
return f"[{self._subject} {self._facet_data}]"
61+
62+
@staticmethod
63+
def of(data):
64+
if isinstance(data, bytes):
65+
# Handling binary data
66+
return Snapshot(SnapshotValue.of(data), {})
67+
elif isinstance(data, str):
68+
# Handling string data
69+
return Snapshot(SnapshotValue.of(data), {})
70+
elif isinstance(data, SnapshotValue):
71+
return Snapshot(data, {})
72+
else:
73+
raise TypeError("Data must be either binary or string" + data)
74+
75+
@staticmethod
76+
def of_entries(entries):
77+
subject = None
78+
facet_data = {}
79+
for key, value in entries:
80+
if not key:
81+
if subject is not None:
82+
raise ValueError(
83+
f"Duplicate root snapshot.\n first: {subject}\nsecond: {value}"
84+
)
85+
subject = value
86+
else:
87+
facet_data[key] = value
88+
if subject is None:
89+
subject = SnapshotValue.of("")
90+
return Snapshot(subject, facet_data)
91+
92+
@staticmethod
93+
def _unix_newlines(string):
94+
return string.replace("\\r\\n", "\\n")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from .Snapshot import Snapshot
2+
3+
4+
class SnapshotReader:
5+
def __init__(self, value_reader):
6+
self.value_reader = value_reader
7+
8+
def peek_key(self):
9+
next_key = self.value_reader.peek_key()
10+
if next_key is None or next_key == "[end of file]":
11+
return None
12+
if "[" in next_key:
13+
raise ValueError(
14+
f"Missing root snapshot, square brackets not allowed: '{next_key}'"
15+
)
16+
return next_key
17+
18+
def next_snapshot(self):
19+
root_name = self.peek_key()
20+
snapshot = Snapshot.of(self.value_reader.next_value())
21+
while True:
22+
next_key = self.value_reader.peek_key()
23+
if next_key is None:
24+
return snapshot
25+
facet_idx = next_key.find("[")
26+
if facet_idx == -1 or (facet_idx == 0 and next_key == "[end of file]"):
27+
return snapshot
28+
facet_root = next_key[:facet_idx]
29+
if facet_root != root_name:
30+
raise ValueError(
31+
f"Expected '{next_key}' to come after '{facet_root}', not '{root_name}'"
32+
)
33+
facet_end_idx = next_key.find("]", facet_idx + 1)
34+
if facet_end_idx == -1:
35+
raise ValueError(f"Missing ] in {next_key}")
36+
facet_name = next_key[facet_idx + 1 : facet_end_idx]
37+
snapshot = snapshot.plus_facet(facet_name, self.value_reader.next_value())
38+
39+
def skip_snapshot(self):
40+
root_name = self.peek_key()
41+
if root_name is None:
42+
raise ValueError("No snapshot to skip")
43+
self.value_reader.skip_value()
44+
while True:
45+
next_key = self.peek_key()
46+
if next_key is None or not next_key.startswith(f"{root_name}["):
47+
break
48+
self.value_reader.skip_value()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
from abc import ABC, abstractmethod
2+
3+
4+
def unix_newlines(string: str) -> str:
5+
return string.replace("\r\n", "\n")
6+
7+
8+
class SnapshotValue(ABC):
9+
@property
10+
def is_binary(self) -> bool:
11+
return isinstance(self, SnapshotValueBinary)
12+
13+
@abstractmethod
14+
def value_binary(self) -> bytes:
15+
pass
16+
17+
@abstractmethod
18+
def value_string(self) -> str:
19+
pass
20+
21+
@staticmethod
22+
def of(data):
23+
if isinstance(data, bytes):
24+
return SnapshotValueBinary(data)
25+
elif isinstance(data, str):
26+
return SnapshotValueString(data)
27+
elif isinstance(data, SnapshotValue):
28+
return data
29+
else:
30+
raise TypeError("Unsupported type for Snapshot creation")
31+
32+
33+
class SnapshotValueBinary(SnapshotValue):
34+
def __init__(self, value: bytes):
35+
self._value = value
36+
37+
def value_binary(self) -> bytes:
38+
return self._value
39+
40+
def value_string(self) -> str:
41+
raise NotImplementedError("This is a binary value.")
42+
43+
def __eq__(self, other):
44+
if isinstance(other, SnapshotValueBinary):
45+
return self.value_binary() == other.value_binary()
46+
return False
47+
48+
def __hash__(self):
49+
return hash(self._value)
50+
51+
52+
class SnapshotValueString(SnapshotValue):
53+
def __init__(self, value: str):
54+
self._value = value
55+
56+
def value_binary(self) -> bytes:
57+
raise NotImplementedError("This is a string value.")
58+
59+
def value_string(self) -> str:
60+
return self._value
61+
62+
def __eq__(self, other):
63+
if isinstance(other, SnapshotValueString):
64+
return self.value_string() == other.value_string()
65+
return False
66+
67+
def __hash__(self):
68+
return hash(self._value)

python/selfie-lib/selfie_lib/SnapshotValueReader.py

+1-48
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,14 @@
11
import base64
2-
3-
from abc import ABC, abstractmethod
4-
from typing import Union
52
from .PerCharacterEscaper import PerCharacterEscaper
63
from .ParseException import ParseException
74
from .LineReader import LineReader
5+
from .SnapshotValue import SnapshotValue
86

97

108
def unix_newlines(string: str) -> str:
119
return string.replace("\r\n", "\n")
1210

1311

14-
class SnapshotValue(ABC):
15-
@property
16-
def is_binary(self) -> bool:
17-
return isinstance(self, SnapshotValueBinary)
18-
19-
@abstractmethod
20-
def value_binary(self) -> bytes:
21-
pass
22-
23-
@abstractmethod
24-
def value_string(self) -> str:
25-
pass
26-
27-
@staticmethod
28-
def of(value: Union[bytes, str]) -> "SnapshotValue":
29-
if isinstance(value, bytes):
30-
return SnapshotValueBinary(value)
31-
elif isinstance(value, str):
32-
return SnapshotValueString(unix_newlines(value))
33-
else:
34-
raise TypeError("Value must be either bytes or str")
35-
36-
37-
class SnapshotValueBinary(SnapshotValue):
38-
def __init__(self, value: bytes):
39-
self._value = value
40-
41-
def value_binary(self) -> bytes:
42-
return self._value
43-
44-
def value_string(self) -> str:
45-
raise NotImplementedError("This is a binary value.")
46-
47-
48-
class SnapshotValueString(SnapshotValue):
49-
def __init__(self, value: str):
50-
self._value = value
51-
52-
def value_binary(self) -> bytes:
53-
raise NotImplementedError("This is a string value.")
54-
55-
def value_string(self) -> str:
56-
return self._value
57-
58-
5912
class SnapshotValueReader:
6013
KEY_FIRST_CHAR = "╔"
6114
KEY_START = "╔═ "

python/selfie-lib/selfie_lib/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@
44
from .PerCharacterEscaper import PerCharacterEscaper as PerCharacterEscaper
55
from .SnapshotValueReader import SnapshotValueReader as SnapshotValueReader
66
from .ParseException import ParseException as ParseException
7+
from .SnapshotReader import SnapshotReader as SnapshotReader
8+
from .Snapshot import Snapshot as Snapshot
9+
from .SnapshotValue import SnapshotValue as SnapshotValue
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from base64 import b64decode
2+
from selfie_lib import SnapshotValueReader, Snapshot, SnapshotReader
3+
4+
5+
class TestSnapshotReader:
6+
def test_facet(self):
7+
reader = SnapshotReader(
8+
SnapshotValueReader.of(
9+
"""
10+
╔═ Apple ═╗
11+
Apple
12+
╔═ Apple[color] ═╗
13+
green
14+
╔═ Apple[crisp] ═╗
15+
yes
16+
╔═ Orange ═╗
17+
Orange
18+
""".strip()
19+
)
20+
)
21+
assert reader.peek_key() == "Apple"
22+
assert reader.peek_key() == "Apple"
23+
apple_snapshot = (
24+
Snapshot.of("Apple").plus_facet("color", "green").plus_facet("crisp", "yes")
25+
)
26+
assert reader.next_snapshot() == apple_snapshot
27+
assert reader.peek_key() == "Orange"
28+
assert reader.peek_key() == "Orange"
29+
assert reader.next_snapshot() == Snapshot.of("Orange")
30+
assert reader.peek_key() is None
31+
32+
def test_binary(self):
33+
reader = SnapshotReader(
34+
SnapshotValueReader.of(
35+
"""
36+
╔═ Apple ═╗
37+
Apple
38+
╔═ Apple[color] ═╗ base64 length 3 bytes
39+
c2Fk
40+
╔═ Apple[crisp] ═╗
41+
yes
42+
╔═ Orange ═╗ base64 length 3 bytes
43+
c2Fk
44+
""".strip()
45+
)
46+
)
47+
assert reader.peek_key() == "Apple"
48+
assert reader.peek_key() == "Apple"
49+
apple_snapshot = (
50+
Snapshot.of("Apple")
51+
.plus_facet("color", b64decode("c2Fk"))
52+
.plus_facet("crisp", "yes")
53+
)
54+
assert reader.next_snapshot() == apple_snapshot
55+
assert reader.peek_key() == "Orange"
56+
assert reader.peek_key() == "Orange"
57+
assert reader.next_snapshot() == Snapshot.of(b64decode("c2Fk"))
58+
assert reader.peek_key() is None

0 commit comments

Comments
 (0)