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

Commit 08ba36e

Browse files
committedMar 20, 2024
Merge branch 'main-python' into main
2 parents 43b7d1a + 8f7528a commit 08ba36e

22 files changed

+1473
-25
lines changed
 

‎.github/workflows/python-ci.yml

+4-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ on:
33
branches: [main]
44
pull_request:
55
paths:
6-
- 'python/**'
6+
- "python/**"
77
defaults:
88
run:
99
working-directory: python/selfie-lib
@@ -24,9 +24,9 @@ jobs:
2424
- name: Set up Python
2525
uses: actions/setup-python@v5
2626
with:
27-
python-version-file: 'python/selfie-lib/pyproject.toml'
28-
cache: 'poetry'
27+
python-version-file: "python/selfie-lib/pyproject.toml"
28+
cache: "poetry"
2929
- run: poetry install
3030
- run: poetry run pytest -vv
3131
- run: poetry run pyright
32-
- run: poetry run ruff check
32+
- run: poetry run ruff format --check

‎.vscode/settings.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"java.compile.nullAnalysis.mode": "automatic"
3+
}
+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
from collections.abc import Set, Iterator, Mapping
2+
from typing import List, TypeVar, Union
3+
from abc import abstractmethod, ABC
4+
5+
T = TypeVar("T")
6+
V = TypeVar("V")
7+
K = TypeVar("K")
8+
9+
10+
class ListBackedSet(Set[T], ABC):
11+
@abstractmethod
12+
def __len__(self) -> int:
13+
...
14+
15+
@abstractmethod
16+
def __getitem__(self, index: Union[int, slice]) -> Union[T, List[T]]:
17+
...
18+
19+
def __contains__(self, item: object) -> bool:
20+
for i in range(len(self)):
21+
if self[i] == item:
22+
return True
23+
return False
24+
25+
26+
class ArraySet(ListBackedSet[K]):
27+
__data: List[K]
28+
29+
def __init__(self, data: List[K]):
30+
raise NotImplementedError("Use ArraySet.empty() instead")
31+
32+
@classmethod
33+
def __create(cls, data: List[K]) -> "ArraySet[K]":
34+
# Create a new instance without calling __init__
35+
instance = super().__new__(cls)
36+
instance.__data = data
37+
return instance
38+
39+
def __iter__(self) -> Iterator[K]:
40+
return iter(self.__data)
41+
42+
@classmethod
43+
def empty(cls) -> "ArraySet[K]":
44+
if not hasattr(cls, "__EMPTY"):
45+
cls.__EMPTY = cls([])
46+
return cls.__EMPTY
47+
48+
def __len__(self) -> int:
49+
return len(self.__data)
50+
51+
def __getitem__(self, index: Union[int, slice]) -> Union[K, List[K]]:
52+
if isinstance(index, int):
53+
return self.__data[index]
54+
elif isinstance(index, slice):
55+
return self.__data[index]
56+
else:
57+
raise TypeError("Invalid argument type.")
58+
59+
def plusOrThis(self, element: K) -> "ArraySet[K]":
60+
# TODO: use binary search, and also special sort order for strings
61+
if element in self.__data:
62+
return self
63+
else:
64+
new_data = self.__data[:]
65+
new_data.append(element)
66+
new_data.sort() # type: ignore[reportOperatorIssue]
67+
return ArraySet.__create(new_data)
68+
69+
70+
class ArrayMap(Mapping[K, V]):
71+
def __init__(self, data: list):
72+
# TODO: hide this constructor as done in ArraySet
73+
self.__data = data
74+
75+
@classmethod
76+
def empty(cls) -> "ArrayMap[K, V]":
77+
if not hasattr(cls, "__EMPTY"):
78+
cls.__EMPTY = cls([])
79+
return cls.__EMPTY
80+
81+
def __getitem__(self, key: K) -> V:
82+
index = self.__binary_search_key(key)
83+
if index >= 0:
84+
return self.__data[2 * index + 1]
85+
raise KeyError(key)
86+
87+
def __iter__(self) -> Iterator[K]:
88+
return (self.__data[i] for i in range(0, len(self.__data), 2))
89+
90+
def __len__(self) -> int:
91+
return len(self.__data) // 2
92+
93+
def __binary_search_key(self, key: K) -> int:
94+
# TODO: special sort order for strings
95+
low, high = 0, (len(self.__data) // 2) - 1
96+
while low <= high:
97+
mid = (low + high) // 2
98+
mid_key = self.__data[2 * mid]
99+
if mid_key < key:
100+
low = mid + 1
101+
elif mid_key > key:
102+
high = mid - 1
103+
else:
104+
return mid
105+
return -(low + 1)
106+
107+
def plus(self, key: K, value: V) -> "ArrayMap[K, V]":
108+
index = self.__binary_search_key(key)
109+
if index >= 0:
110+
raise ValueError("Key already exists")
111+
insert_at = -(index + 1)
112+
new_data = self.__data[:]
113+
new_data[insert_at * 2 : insert_at * 2] = [key, value]
114+
return ArrayMap(new_data)
115+
116+
def minus_sorted_indices(self, indicesToRemove: List[int]) -> "ArrayMap[K, V]":
117+
if not indicesToRemove:
118+
return self
119+
newData = []
120+
for i in range(0, len(self.__data), 2):
121+
if i // 2 not in indicesToRemove:
122+
newData.extend(self.__data[i : i + 2])
123+
return ArrayMap(newData)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
from typing import Dict, Iterable, Tuple
2+
from enum import Enum, auto
3+
import threading
4+
from selfie_lib.TypedPath import TypedPath
5+
from selfie_lib.Slice import Slice
6+
7+
8+
# Placeholder implementations for CallStack, SnapshotFileLayout, and FS
9+
class CallStack:
10+
pass
11+
12+
13+
class SnapshotFileLayout:
14+
def sourcePathForCall(self, location) -> "TypedPath":
15+
# Placeholder return or raise NotImplementedError
16+
raise NotImplementedError("sourcePathForCall is not implemented")
17+
18+
19+
class WritableComment(Enum):
20+
NO_COMMENT = auto()
21+
ONCE = auto()
22+
FOREVER = auto()
23+
24+
@property
25+
def writable(self) -> bool:
26+
return self != WritableComment.NO_COMMENT
27+
28+
29+
class CommentTracker:
30+
def __init__(self):
31+
self.cache: Dict[TypedPath, WritableComment] = {}
32+
self.lock = threading.Lock()
33+
34+
def pathsWithOnce(self) -> Iterable[TypedPath]:
35+
with self.lock:
36+
return [
37+
path
38+
for path, comment in self.cache.items()
39+
if comment == WritableComment.ONCE
40+
]
41+
42+
def hasWritableComment(self, call: CallStack, layout: SnapshotFileLayout) -> bool:
43+
path = layout.sourcePathForCall(call)
44+
with self.lock:
45+
if path in self.cache:
46+
comment = self.cache[path]
47+
if comment.writable:
48+
return True
49+
else:
50+
return False
51+
else:
52+
new_comment, _ = self.__commentAndLine(path)
53+
self.cache[path] = new_comment
54+
return new_comment.writable
55+
56+
@staticmethod
57+
def commentString(typedPath: TypedPath) -> Tuple[str, int]:
58+
comment, line = CommentTracker.__commentAndLine(typedPath)
59+
if comment == WritableComment.NO_COMMENT:
60+
raise ValueError("No writable comment found")
61+
elif comment == WritableComment.ONCE:
62+
return ("//selfieonce", line)
63+
elif comment == WritableComment.FOREVER:
64+
return ("//SELFIEWRITE", line)
65+
else:
66+
raise ValueError("Invalid comment type")
67+
68+
@staticmethod
69+
def __commentAndLine(typedPath: TypedPath) -> Tuple[WritableComment, int]:
70+
with open(typedPath.absolute_path, "r") as file:
71+
content = Slice(file.read())
72+
for comment_str in [
73+
"//selfieonce",
74+
"// selfieonce",
75+
"//SELFIEWRITE",
76+
"// SELFIEWRITE",
77+
]:
78+
index = content.indexOf(comment_str)
79+
if index != -1:
80+
lineNumber = content.baseLineAtOffset(index)
81+
comment = (
82+
WritableComment.ONCE
83+
if "once" in comment_str
84+
else WritableComment.FOREVER
85+
)
86+
return (comment, lineNumber)
87+
return (WritableComment.NO_COMMENT, -1)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from enum import Enum, auto
2+
3+
4+
class EscapeLeadingWhitespace(Enum):
5+
NEVER = auto()
6+
7+
def escape_line(self, line: str, space: str, tab: str) -> str:
8+
return line
9+
10+
@staticmethod
11+
def appropriate_for(file_content: str) -> "EscapeLeadingWhitespace":
12+
return EscapeLeadingWhitespace.NEVER
+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import io
2+
3+
4+
class LineReader:
5+
def __init__(self, content: bytes):
6+
self.__buffer = io.BytesIO(content)
7+
self.__uses_unix_newlines = self.__detect_newline_type()
8+
self.__line_count = 0 # Initialize line count
9+
10+
@classmethod
11+
def for_binary(cls, content: bytes):
12+
return cls(content)
13+
14+
@classmethod
15+
def for_string(cls, content: str):
16+
return cls(content.encode("utf-8"))
17+
18+
def __detect_newline_type(self) -> bool:
19+
first_line = self.__buffer.readline()
20+
self.__buffer.seek(0) # Reset buffer for actual reading
21+
return b"\r\n" not in first_line
22+
23+
def unix_newlines(self) -> bool:
24+
return self.__uses_unix_newlines
25+
26+
def read_line(self) -> str:
27+
line_bytes = self.__buffer.readline()
28+
if line_bytes:
29+
self.__line_count += 1 # Increment line count for each line read
30+
line = line_bytes.decode("utf-8")
31+
return line.rstrip("\r\n" if not self.__uses_unix_newlines else "\n")
32+
33+
# Method to get the current line number
34+
def get_line_number(self) -> int:
35+
return self.__line_count
+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
from enum import Enum, auto
2+
from typing import Protocol, TypeVar
3+
from abc import abstractmethod
4+
from .EscapeLeadingWhitespace import EscapeLeadingWhitespace
5+
import io
6+
7+
T = TypeVar("T")
8+
9+
10+
class Language(Enum):
11+
PYTHON = auto()
12+
13+
@classmethod
14+
def from_filename(cls, filename: str) -> "Language":
15+
extension = filename.rsplit(".", 1)[-1]
16+
if extension == "py":
17+
return cls.PYTHON
18+
else:
19+
raise ValueError(f"Unknown language for file {filename}")
20+
21+
22+
class LiteralValue:
23+
def __init__(self, expected: T | None, actual: T, format: "LiteralFormat") -> None:
24+
self.expected = expected
25+
self.actual = actual
26+
self.format = format
27+
28+
29+
class LiteralFormat(Protocol[T]):
30+
@abstractmethod
31+
def encode(
32+
self, value: T, language: Language, encoding_policy: "EscapeLeadingWhitespace"
33+
) -> str:
34+
raise NotImplementedError("Subclasses must implement the encode method")
35+
36+
@abstractmethod
37+
def parse(self, string: str, language: Language) -> T:
38+
raise NotImplementedError("Subclasses must implement the parse method")
39+
40+
41+
MAX_RAW_NUMBER = 1000
42+
PADDING_SIZE = len(str(MAX_RAW_NUMBER)) - 1
43+
44+
45+
class LiteralInt(LiteralFormat[int]):
46+
def _encode_underscores(
47+
self, buffer: io.StringIO, value: int, language: Language
48+
) -> io.StringIO:
49+
if value >= MAX_RAW_NUMBER:
50+
mod = value % MAX_RAW_NUMBER
51+
left_padding = PADDING_SIZE - len(str(mod))
52+
self._encode_underscores(buffer, value // MAX_RAW_NUMBER, language)
53+
buffer.write("_")
54+
buffer.write("0" * left_padding)
55+
buffer.write(str(mod))
56+
return buffer
57+
elif value < 0:
58+
buffer.write("-")
59+
self._encode_underscores(buffer, abs(value), language)
60+
return buffer
61+
else:
62+
buffer.write(str(value))
63+
return buffer
64+
65+
def encode(
66+
self, value: int, language: Language, encoding_policy: EscapeLeadingWhitespace
67+
) -> str:
68+
return self._encode_underscores(io.StringIO(), value, language).getvalue()
69+
70+
def parse(self, string: str, language: Language) -> int:
71+
return int(string.replace("_", ""))
72+
73+
74+
class LiteralBoolean(LiteralFormat[bool]):
75+
def encode(
76+
self, value: bool, language: Language, encoding_policy: EscapeLeadingWhitespace
77+
) -> str:
78+
return str(value)
79+
80+
def __to_boolean_strict(self, string: str) -> bool:
81+
if string.lower() == "true":
82+
return True
83+
elif string.lower() == "false":
84+
return False
85+
else:
86+
raise ValueError("String is not a valid boolean representation: " + string)
87+
88+
def parse(self, string: str, language: Language) -> bool:
89+
return self.__to_boolean_strict(string)

0 commit comments

Comments
 (0)