From 50c908b49f6f493b430958d028e90301d0b85f5b Mon Sep 17 00:00:00 2001 From: Edwin Date: Wed, 21 Feb 2024 16:59:28 -0800 Subject: [PATCH 01/73] make some changes to edwin-pr --- python/selfie-lib/selfie_lib/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python/selfie-lib/selfie_lib/__init__.py b/python/selfie-lib/selfie_lib/__init__.py index 6f2000ed..a6640182 100644 --- a/python/selfie-lib/selfie_lib/__init__.py +++ b/python/selfie-lib/selfie_lib/__init__.py @@ -1,2 +1,3 @@ from .ned import fizzbuzz as fizzbuzz from .harvir import silly_addition as silly_addition +from .edwin import silly_subtraction as silly_subtraction \ No newline at end of file From 041af0272ef1b3f8c5bdb10602d882916f0482c4 Mon Sep 17 00:00:00 2001 From: Edwin Date: Thu, 22 Feb 2024 00:22:03 -0800 Subject: [PATCH 02/73] Resolved merge conflicts --- python/selfie-lib/selfie_lib/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/selfie-lib/selfie_lib/__init__.py b/python/selfie-lib/selfie_lib/__init__.py index a6640182..c69baa72 100644 --- a/python/selfie-lib/selfie_lib/__init__.py +++ b/python/selfie-lib/selfie_lib/__init__.py @@ -1,3 +1,3 @@ from .ned import fizzbuzz as fizzbuzz from .harvir import silly_addition as silly_addition -from .edwin import silly_subtraction as silly_subtraction \ No newline at end of file +from .edwin import simple_subtraction as simple_subtraction \ No newline at end of file From f4dfcce48d8fe5ef69187423ba84a0a36a4f64a9 Mon Sep 17 00:00:00 2001 From: Edwin Date: Thu, 22 Feb 2024 12:19:15 -0800 Subject: [PATCH 03/73] Trickybrain Adding LineReader --- python/selfie-lib/selfie_lib/LineReader.py | 48 ++++++++++++++++++++++ python/selfie-lib/selfie_lib/__init__.py | 3 +- python/selfie-lib/tests/lineReaderTest.py | 45 ++++++++++++++++++++ 3 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 python/selfie-lib/selfie_lib/LineReader.py create mode 100644 python/selfie-lib/tests/lineReaderTest.py diff --git a/python/selfie-lib/selfie_lib/LineReader.py b/python/selfie-lib/selfie_lib/LineReader.py new file mode 100644 index 00000000..5ff7b35e --- /dev/null +++ b/python/selfie-lib/selfie_lib/LineReader.py @@ -0,0 +1,48 @@ + +from typing import Optional, BinaryIO +from io import StringIO, BufferedReader + +class LineTerminatorReader: + def __init__(self, reader: BufferedReader): + self.reader = reader + self.unix_newlines = True + + def read(self, size: int = -1) -> str: + result = self.reader.read(size) + self.unix_newlines = '\r' not in result + return result + + def read_line(self) -> Optional[str]: + line = self.reader.readline() + if line == '': + return None + self.unix_newlines = '\r' not in line + return line + + def unix_newlines(self) -> bool: + return self.unix_newlines + +class LineReader: + def __init__(self, reader: BufferedReader): + self.reader = LineTerminatorReader(reader) + self.line_number = 0 + + @classmethod + def for_string(cls, content: str) -> 'LineReader': + return cls(StringIO(content)) + + @classmethod + def for_binary(cls, content: bytes) -> 'LineReader': + return cls(BufferedReader(content)) + + def get_line_number(self) -> int: + return self.line_number + + def read_line(self) -> Optional[str]: + line = self.reader.read_line() + if line is not None: + self.line_number += 1 + return line + + def unix_newlines(self) -> bool: + return self.reader.unix_newlines() diff --git a/python/selfie-lib/selfie_lib/__init__.py b/python/selfie-lib/selfie_lib/__init__.py index c69baa72..5ffc0c82 100644 --- a/python/selfie-lib/selfie_lib/__init__.py +++ b/python/selfie-lib/selfie_lib/__init__.py @@ -1,3 +1,4 @@ from .ned import fizzbuzz as fizzbuzz from .harvir import silly_addition as silly_addition -from .edwin import simple_subtraction as simple_subtraction \ No newline at end of file +from .edwin import simple_subtraction as simple_subtraction +from .LineReader import LineReader as LineReader \ No newline at end of file diff --git a/python/selfie-lib/tests/lineReaderTest.py b/python/selfie-lib/tests/lineReaderTest.py new file mode 100644 index 00000000..167ad398 --- /dev/null +++ b/python/selfie-lib/tests/lineReaderTest.py @@ -0,0 +1,45 @@ +from selfie_lib import LineReader + +# Assuming the LineReader class is defined as previously translated +# Adjustments may be needed if the LineReader implementation differs + +def test_should_find_unix_separator_from_binary(): + reader = LineReader.for_binary(b"This is a new line\n") + assert reader.unix_newlines() == True + assert reader.read_line() == "This is a new line" + +def test_should_find_windows_separator_from_binary(): + reader = LineReader.for_binary(b"This is a new line\r\n") + assert reader.unix_newlines() == False + assert reader.read_line() == "This is a new line" + +def test_should_find_unix_separator_from_string(): + reader = LineReader.for_string("This is a new line\n") + assert reader.unix_newlines() == True + assert reader.read_line() == "This is a new line" + +def test_should_find_windows_separator_from_string(): + reader = LineReader.for_string("This is a new line\r\n") + assert reader.unix_newlines() is False + assert reader.read_line() == "This is a new line" + +def test_should_get_unix_line_separator_when_there_is_none(): + reader = LineReader.for_binary(b"This is a new line") + assert reader.unix_newlines() is True + assert reader.read_line() == "This is a new line" + +def test_should_read_next_line_without_problem(): + reader = LineReader.for_binary(b"First\r\nSecond\r\n") + assert reader.unix_newlines() == False + assert reader.read_line() == "First" + assert reader.unix_newlines() == False + assert reader.read_line() == "Second" + assert reader.unix_newlines() == False + +def test_should_use_first_line_separator_and_ignore_next(): + reader = LineReader.for_binary(b"First\r\nAnother separator\n") + assert reader.unix_newlines() == False + assert reader.read_line() == "First" + assert reader.unix_newlines() == False + assert reader.read_line() == "Another separator" + assert reader.unix_newlines() == False From 9fa63524dc9f0121884c5982b33d57d50ca7c740 Mon Sep 17 00:00:00 2001 From: Edwin Date: Thu, 22 Feb 2024 12:29:04 -0800 Subject: [PATCH 04/73] Make changes to LineReader, fixing the type issue --- python/selfie-lib/selfie_lib/LineReader.py | 50 +++++++++++----------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/python/selfie-lib/selfie_lib/LineReader.py b/python/selfie-lib/selfie_lib/LineReader.py index 5ff7b35e..647886d4 100644 --- a/python/selfie-lib/selfie_lib/LineReader.py +++ b/python/selfie-lib/selfie_lib/LineReader.py @@ -1,48 +1,48 @@ +from io import StringIO, BufferedReader, TextIOWrapper +import io -from typing import Optional, BinaryIO -from io import StringIO, BufferedReader - -class LineTerminatorReader: - def __init__(self, reader: BufferedReader): - self.reader = reader +class LineTerminatorReader(BufferedReader): + def __init__(self, reader): + super().__init__(reader) self.unix_newlines = True - def read(self, size: int = -1) -> str: - result = self.reader.read(size) - self.unix_newlines = '\r' not in result - return result + def read(self, size=-1): + chunk = super().read(size) + if '\r' in chunk: + self.unix_newlines = False + return chunk - def read_line(self) -> Optional[str]: - line = self.reader.readline() - if line == '': - return None - self.unix_newlines = '\r' not in line - return line + def read_line(self): + line = super().readline() + if '\r' in line: + self.unix_newlines = False + return line.rstrip('\n').rstrip('\r') - def unix_newlines(self) -> bool: + def unix_newlines(self): return self.unix_newlines class LineReader: - def __init__(self, reader: BufferedReader): + def __init__(self, reader): self.reader = LineTerminatorReader(reader) self.line_number = 0 @classmethod - def for_string(cls, content: str) -> 'LineReader': + def for_string(cls, content): return cls(StringIO(content)) @classmethod - def for_binary(cls, content: bytes) -> 'LineReader': - return cls(BufferedReader(content)) + def for_binary(cls, content): + return cls(TextIOWrapper(io.BytesIO(content), encoding='utf-8')) - def get_line_number(self) -> int: + def get_line_number(self): return self.line_number - def read_line(self) -> Optional[str]: + def read_line(self): line = self.reader.read_line() - if line is not None: + if line: self.line_number += 1 return line - def unix_newlines(self) -> bool: + def unix_newlines(self): return self.reader.unix_newlines() + From 2005fdbdc0e32eb2adc067fedc8049487938918f Mon Sep 17 00:00:00 2001 From: Edwin Date: Thu, 22 Feb 2024 12:34:22 -0800 Subject: [PATCH 05/73] Make changes to assigned in python --- python/selfie-lib/selfie_lib/LineReader.py | 46 +++++++++++----------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/python/selfie-lib/selfie_lib/LineReader.py b/python/selfie-lib/selfie_lib/LineReader.py index 647886d4..154ae910 100644 --- a/python/selfie-lib/selfie_lib/LineReader.py +++ b/python/selfie-lib/selfie_lib/LineReader.py @@ -1,48 +1,48 @@ -from io import StringIO, BufferedReader, TextIOWrapper import io -class LineTerminatorReader(BufferedReader): +class LineTerminatorAware: def __init__(self, reader): - super().__init__(reader) + self.reader = reader + self.line_number = 0 self.unix_newlines = True - - def read(self, size=-1): - chunk = super().read(size) - if '\r' in chunk: + self.first_line = self.reader.readline() + if '\r' in self.first_line: self.unix_newlines = False - return chunk def read_line(self): - line = super().readline() - if '\r' in line: - self.unix_newlines = False - return line.rstrip('\n').rstrip('\r') + if self.first_line: + line = self.first_line + self.first_line = None + self.line_number += 1 + return line + line = self.reader.readline() + if line: + if '\r' in line: + self.unix_newlines = False + self.line_number += 1 + return line def unix_newlines(self): return self.unix_newlines + class LineReader: def __init__(self, reader): - self.reader = LineTerminatorReader(reader) - self.line_number = 0 + self.reader = LineTerminatorAware(reader) @classmethod def for_string(cls, content): - return cls(StringIO(content)) + return cls(io.StringIO(content)) @classmethod def for_binary(cls, content): - return cls(TextIOWrapper(io.BytesIO(content), encoding='utf-8')) + return cls(io.BytesIO(content).read().decode('utf-8')) def get_line_number(self): - return self.line_number + return self.reader.line_number def read_line(self): - line = self.reader.read_line() - if line: - self.line_number += 1 - return line + return self.reader.read_line() def unix_newlines(self): - return self.reader.unix_newlines() - + return self.reader.unix_newlines() \ No newline at end of file From 96b946ef9a36d94128d7b1232592d9299f482189 Mon Sep 17 00:00:00 2001 From: Harvir Sahota Date: Sun, 25 Feb 2024 18:37:23 -0800 Subject: [PATCH 06/73] Add ArrayMap and ArrayMap_Test --- .vscode/settings.json | 3 + python/selfie-lib/selfie_lib/ArrayMap.py | 146 +++++++++++++++++++++++ python/selfie-lib/tests/ArrayMap_test.py | 87 ++++++++++++++ 3 files changed, 236 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 python/selfie-lib/selfie_lib/ArrayMap.py create mode 100644 python/selfie-lib/tests/ArrayMap_test.py diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..7b016a89 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.compile.nullAnalysis.mode": "automatic" +} \ No newline at end of file diff --git a/python/selfie-lib/selfie_lib/ArrayMap.py b/python/selfie-lib/selfie_lib/ArrayMap.py new file mode 100644 index 00000000..b0f739f1 --- /dev/null +++ b/python/selfie-lib/selfie_lib/ArrayMap.py @@ -0,0 +1,146 @@ +from collections.abc import Set, Sequence, Iterator, Mapping +from typing import TypeVar, List +from functools import cmp_to_key + +T = TypeVar('T') +K = TypeVar('K') +V = TypeVar('V') + +class ListBackedSet(Set[T], Sequence[T]): + def __getitem__(self, index: int) -> T: + # This method should be implemented by the subclass. + raise NotImplementedError + + def __len__(self) -> int: + # This should also be implemented by the subclass to return the number of items. + raise NotImplementedError + + def __iter__(self) -> Iterator[T]: + return self.ListBackedSetIterator(self) + + class ListBackedSetIterator(Iterator[T]): + def __init__(self, list_backed_set: 'ListBackedSet[T]'): + self.list_backed_set = list_backed_set + self.index = 0 + + def __next__(self) -> T: + if self.index < len(self.list_backed_set): + result = self.list_backed_set[self.index] + self.index += 1 + return result + else: + raise StopIteration + + def __contains__(self, item: object) -> bool: + # Efficient implementation of __contains__ should be provided by subclass if needed. + for i in self: + if i == item: + return True + return False + +class ArraySet(ListBackedSet[K]): + def __init__(self, data: list): + self.data = data + self.sort_data() + + def sort_data(self): + if self.data and isinstance(self.data[0], str): + self.data.sort(key=cmp_to_key(self.string_slash_first_comparator)) + else: + self.data.sort() + + def string_slash_first_comparator(self, a, b): + # Define sorting where '/' is considered the lowest key + if a == '/': + return -1 + elif b == '/': + return 1 + else: + return (a > b) - (a < b) + + def __len__(self): + return len(self.data) + + def __getitem__(self, index: int) -> K: + return self.data[index] + + def __contains__(self, item: K) -> bool: + # Implementing binary search for efficiency + left, right = 0, len(self.data) - 1 + while left <= right: + mid = (left + right) // 2 + if self.data[mid] == item: + return True + elif self.data[mid] < item: + left = mid + 1 + else: + right = mid - 1 + return False + + def plus_or_this(self, key: K) -> 'ArraySet[K]': + # Binary search to find the appropriate index or confirm existence + left, right = 0, len(self.data) - 1 + while left <= right: + mid = (left + right) // 2 + if self.data[mid] == key: + return self # Key already exists + elif self.data[mid] < key: + left = mid + 1 + else: + right = mid - 1 + + # Key does not exist, insert in the sorted position + new_data = self.data[:left] + [key] + self.data[left:] + return ArraySet(new_data) + +class ArrayMap(Mapping[K, V]): + def __init__(self, data: list): + self.data = data + + @classmethod + def empty(cls): + return cls([]) + + def __getitem__(self, key: K) -> V: + index = self._binary_search_key(key) + if index >= 0: + return self.data[2 * index + 1] + raise KeyError(key) + + def __iter__(self): + return (self.data[i] for i in range(0, len(self.data), 2)) + + def __len__(self) -> int: + return len(self.data) // 2 + + def _binary_search_key(self, key: K) -> int: + low, high = 0, len(self.data) // 2 - 1 + while low <= high: + mid = (low + high) // 2 + mid_key = self.data[2 * mid] + if mid_key < key: + low = mid + 1 + elif mid_key > key: + high = mid - 1 + else: + return mid + return -(low + 1) + + def plus(self, key: K, value: V) -> 'ArrayMap[K, V]': + index = self._binary_search_key(key) + if index >= 0: + raise ValueError("Key already exists") + else: + insert_at = -(index + 1) + new_data = self.data[:] + new_data[insert_at * 2:insert_at * 2] = [key, value] + return ArrayMap(new_data) + + def minus_sorted_indices(self, indicesToRemove: list[int]) -> 'ArrayMap[K, V]': + if not indicesToRemove: + return self + newData = [] + for i in range(0, len(self.data), 2): + if i // 2 not in indicesToRemove: + newData.extend(self.data[i:i + 2]) + return ArrayMap(newData) diff --git a/python/selfie-lib/tests/ArrayMap_test.py b/python/selfie-lib/tests/ArrayMap_test.py new file mode 100644 index 00000000..7e2a1e98 --- /dev/null +++ b/python/selfie-lib/tests/ArrayMap_test.py @@ -0,0 +1,87 @@ +import unittest +from selfie_lib.ArrayMap import ArrayMap + +class ArrayMapTest(unittest.TestCase): + def assertEmpty(self, map): + self.assertEqual(len(map), 0) + self.assertEqual(list(map.keys()), []) + self.assertEqual(list(map.values()), []) + self.assertEqual(list(map.items()), []) + with self.assertRaises(KeyError): + _ = map["key"] + self.assertEqual(map, {}) + self.assertEqual(map, ArrayMap.empty()) + + def assertSingle(self, map, key, value): + self.assertEqual(len(map), 1) + self.assertEqual(set(map.keys()), {key}) + self.assertEqual(list(map.values()), [value]) + self.assertEqual(set(map.items()), {(key, value)}) + self.assertEqual(map[key], value) + with self.assertRaises(KeyError): + _ = map[key + "blah"] + self.assertEqual(map, {key: value}) + self.assertEqual(map, ArrayMap.empty().plus(key, value)) + + def assertDouble(self, map, key1, value1, key2, value2): + self.assertEqual(len(map), 2) + self.assertEqual(set(map.keys()), {key1, key2}) + self.assertEqual(list(map.values()), [value1, value2]) + self.assertEqual(set(map.items()), {(key1, value1), (key2, value2)}) + self.assertEqual(map[key1], value1) + self.assertEqual(map[key2], value2) + with self.assertRaises(KeyError): + _ = map[key1 + "blah"] + self.assertEqual(map, {key1: value1, key2: value2}) + self.assertEqual(map, {key2: value2, key1: value1}) + self.assertEqual(map, ArrayMap.empty().plus(key1, value1).plus(key2, value2)) + self.assertEqual(map, ArrayMap.empty().plus(key2, value2).plus(key1, value1)) + + def assertTriple(self, map, key1, value1, key2, value2, key3, value3): + self.assertEqual(len(map), 3) + self.assertEqual(set(map.keys()), {key1, key2, key3}) + self.assertEqual(list(map.values()), [value1, value2, value3]) + self.assertEqual(set(map.items()), {(key1, value1), (key2, value2), (key3, value3)}) + self.assertEqual(map[key1], value1) + self.assertEqual(map[key2], value2) + self.assertEqual(map[key3], value3) + with self.assertRaises(KeyError): + _ = map[key1 + "blah"] + self.assertEqual(map, {key1: value1, key2: value2, key3: value3}) + self.assertEqual(map, ArrayMap.empty().plus(key1, value1).plus(key2, value2).plus(key3, value3)) + + def test_empty(self): + self.assertEmpty(ArrayMap.empty()) + + def test_single(self): + empty = ArrayMap.empty() + single = empty.plus("one", "1") + self.assertEmpty(empty) + self.assertSingle(single, "one", "1") + + def test_double(self): + empty = ArrayMap.empty() + single = empty.plus("one", "1") + double = single.plus("two", "2") + self.assertEmpty(empty) + self.assertSingle(single, "one", "1") + self.assertDouble(double, "one", "1", "two", "2") + self.assertDouble(single.plus("a", "sorted"), "a", "sorted", "one", "1") + + with self.assertRaises(ValueError) as context: + single.plus("one", "2") + self.assertEqual(str(context.exception), "Key already exists") + + def test_triple(self): + triple = ArrayMap.empty().plus("1", "one").plus("2", "two").plus("3", "three") + self.assertTriple(triple, "1", "one", "2", "two", "3", "three") + + def test_multi(self): + self.test_triple() + triple = ArrayMap.empty().plus("2", "two").plus("3", "three").plus("1", "one") + self.assertTriple(triple, "1", "one", "2", "two", "3", "three") + triple = ArrayMap.empty().plus("3", "three").plus("1", "one").plus("2", "two") + self.assertTriple(triple, "1", "one", "2", "two", "3", "three") + +if __name__ == '__main__': + unittest.main() From c9fd4734e3448eb3fc0ce89296c2696ee56b1cfd Mon Sep 17 00:00:00 2001 From: Harvir Sahota Date: Sun, 25 Feb 2024 18:42:37 -0800 Subject: [PATCH 07/73] Remove TypeVar import --- python/selfie-lib/selfie_lib/ArrayMap.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/python/selfie-lib/selfie_lib/ArrayMap.py b/python/selfie-lib/selfie_lib/ArrayMap.py index b0f739f1..cc58420d 100644 --- a/python/selfie-lib/selfie_lib/ArrayMap.py +++ b/python/selfie-lib/selfie_lib/ArrayMap.py @@ -1,11 +1,7 @@ from collections.abc import Set, Sequence, Iterator, Mapping -from typing import TypeVar, List +from typing import List from functools import cmp_to_key -T = TypeVar('T') -K = TypeVar('K') -V = TypeVar('V') - class ListBackedSet(Set[T], Sequence[T]): def __getitem__(self, index: int) -> T: # This method should be implemented by the subclass. From abace70c83cbba7492fbb09e4a6035d5b7603d72 Mon Sep 17 00:00:00 2001 From: Harvir Sahota Date: Mon, 26 Feb 2024 14:16:18 -0800 Subject: [PATCH 08/73] Update ArrayMap and ArrayMap_test --- python/selfie-lib/selfie_lib/ArrayMap.py | 86 ++++++------- python/selfie-lib/tests/ArrayMap_test.py | 148 +++++++++++------------ 2 files changed, 115 insertions(+), 119 deletions(-) diff --git a/python/selfie-lib/selfie_lib/ArrayMap.py b/python/selfie-lib/selfie_lib/ArrayMap.py index cc58420d..ddcb31a4 100644 --- a/python/selfie-lib/selfie_lib/ArrayMap.py +++ b/python/selfie-lib/selfie_lib/ArrayMap.py @@ -1,38 +1,37 @@ from collections.abc import Set, Sequence, Iterator, Mapping -from typing import List +from typing import List, TypeVar, Union, overload, Sequence as TypeSequence from functools import cmp_to_key +T = TypeVar('T') +K = TypeVar('K') +V = TypeVar('V') + class ListBackedSet(Set[T], Sequence[T]): - def __getitem__(self, index: int) -> T: - # This method should be implemented by the subclass. - raise NotImplementedError + def __init__(self): + self._list: List[T] = [] + + @overload + def __getitem__(self, index: int) -> T: ... + + @overload + def __getitem__(self, index: slice) -> TypeSequence[T]: ... + + def __getitem__(self, index: Union[int, slice]) -> Union[T, TypeSequence[T]]: + if isinstance(index, int): + return self._list[index] + elif isinstance(index, slice): + return self._list[index] + else: + raise TypeError("Index must be int or slice") def __len__(self) -> int: - # This should also be implemented by the subclass to return the number of items. - raise NotImplementedError + return len(self._list) def __iter__(self) -> Iterator[T]: - return self.ListBackedSetIterator(self) - - class ListBackedSetIterator(Iterator[T]): - def __init__(self, list_backed_set: 'ListBackedSet[T]'): - self.list_backed_set = list_backed_set - self.index = 0 - - def __next__(self) -> T: - if self.index < len(self.list_backed_set): - result = self.list_backed_set[self.index] - self.index += 1 - return result - else: - raise StopIteration + return iter(self._list) def __contains__(self, item: object) -> bool: - # Efficient implementation of __contains__ should be provided by subclass if needed. - for i in self: - if i == item: - return True - return False + return item in self._list class ArraySet(ListBackedSet[K]): def __init__(self, data: list): @@ -46,7 +45,6 @@ def sort_data(self): self.data.sort() def string_slash_first_comparator(self, a, b): - # Define sorting where '/' is considered the lowest key if a == '/': return -1 elif b == '/': @@ -57,35 +55,37 @@ def string_slash_first_comparator(self, a, b): def __len__(self): return len(self.data) - def __getitem__(self, index: int) -> K: - return self.data[index] + @overload + def __getitem__(self, index: int) -> K: ... - def __contains__(self, item: K) -> bool: - # Implementing binary search for efficiency - left, right = 0, len(self.data) - 1 - while left <= right: - mid = (left + right) // 2 - if self.data[mid] == item: - return True - elif self.data[mid] < item: - left = mid + 1 - else: - right = mid - 1 - return False + @overload + def __getitem__(self, index: slice) -> 'ArraySet[K]': ... + + def __getitem__(self, index: Union[int, slice]) -> Union[K, 'ArraySet[K]']: + if isinstance(index, int): + return self.data[index] + elif isinstance(index, slice): + sliced_data = self.data[index] + return ArraySet(sliced_data) + else: + raise TypeError("Index must be int or slice") + + def __contains__(self, item: object) -> bool: + if not isinstance(item, type(self.data[0])): + return False + return super().__contains__(item) def plus_or_this(self, key: K) -> 'ArraySet[K]': - # Binary search to find the appropriate index or confirm existence left, right = 0, len(self.data) - 1 while left <= right: mid = (left + right) // 2 if self.data[mid] == key: - return self # Key already exists + return self elif self.data[mid] < key: left = mid + 1 else: right = mid - 1 - # Key does not exist, insert in the sorted position new_data = self.data[:left] + [key] + self.data[left:] return ArraySet(new_data) diff --git a/python/selfie-lib/tests/ArrayMap_test.py b/python/selfie-lib/tests/ArrayMap_test.py index 7e2a1e98..9d9550ae 100644 --- a/python/selfie-lib/tests/ArrayMap_test.py +++ b/python/selfie-lib/tests/ArrayMap_test.py @@ -1,87 +1,83 @@ -import unittest +import pytest from selfie_lib.ArrayMap import ArrayMap -class ArrayMapTest(unittest.TestCase): - def assertEmpty(self, map): - self.assertEqual(len(map), 0) - self.assertEqual(list(map.keys()), []) - self.assertEqual(list(map.values()), []) - self.assertEqual(list(map.items()), []) - with self.assertRaises(KeyError): - _ = map["key"] - self.assertEqual(map, {}) - self.assertEqual(map, ArrayMap.empty()) +def assertEmpty(map): + assert len(map) == 0 + assert list(map.keys()) == [] + assert list(map.values()) == [] + assert list(map.items()) == [] + with pytest.raises(KeyError): + _ = map["key"] + assert map == {} + assert map == ArrayMap.empty() - def assertSingle(self, map, key, value): - self.assertEqual(len(map), 1) - self.assertEqual(set(map.keys()), {key}) - self.assertEqual(list(map.values()), [value]) - self.assertEqual(set(map.items()), {(key, value)}) - self.assertEqual(map[key], value) - with self.assertRaises(KeyError): - _ = map[key + "blah"] - self.assertEqual(map, {key: value}) - self.assertEqual(map, ArrayMap.empty().plus(key, value)) +def assertSingle(map, key, value): + assert len(map) == 1 + assert set(map.keys()) == {key} + assert list(map.values()) == [value] + assert set(map.items()) == {(key, value)} + assert map[key] == value + with pytest.raises(KeyError): + _ = map[key + "blah"] + assert map == {key: value} + assert map == ArrayMap.empty().plus(key, value) - def assertDouble(self, map, key1, value1, key2, value2): - self.assertEqual(len(map), 2) - self.assertEqual(set(map.keys()), {key1, key2}) - self.assertEqual(list(map.values()), [value1, value2]) - self.assertEqual(set(map.items()), {(key1, value1), (key2, value2)}) - self.assertEqual(map[key1], value1) - self.assertEqual(map[key2], value2) - with self.assertRaises(KeyError): - _ = map[key1 + "blah"] - self.assertEqual(map, {key1: value1, key2: value2}) - self.assertEqual(map, {key2: value2, key1: value1}) - self.assertEqual(map, ArrayMap.empty().plus(key1, value1).plus(key2, value2)) - self.assertEqual(map, ArrayMap.empty().plus(key2, value2).plus(key1, value1)) +def assertDouble(map, key1, value1, key2, value2): + assert len(map) == 2 + assert set(map.keys()) == {key1, key2} + assert list(map.values()) == [value1, value2] + assert set(map.items()) == {(key1, value1), (key2, value2)} + assert map[key1] == value1 + assert map[key2] == value2 + with pytest.raises(KeyError): + _ = map[key1 + "blah"] + assert map == {key1: value1, key2: value2} + assert map == {key2: value2, key1: value1} + assert map == ArrayMap.empty().plus(key1, value1).plus(key2, value2) + assert map == ArrayMap.empty().plus(key2, value2).plus(key1, value1) - def assertTriple(self, map, key1, value1, key2, value2, key3, value3): - self.assertEqual(len(map), 3) - self.assertEqual(set(map.keys()), {key1, key2, key3}) - self.assertEqual(list(map.values()), [value1, value2, value3]) - self.assertEqual(set(map.items()), {(key1, value1), (key2, value2), (key3, value3)}) - self.assertEqual(map[key1], value1) - self.assertEqual(map[key2], value2) - self.assertEqual(map[key3], value3) - with self.assertRaises(KeyError): - _ = map[key1 + "blah"] - self.assertEqual(map, {key1: value1, key2: value2, key3: value3}) - self.assertEqual(map, ArrayMap.empty().plus(key1, value1).plus(key2, value2).plus(key3, value3)) +def assertTriple(map, key1, value1, key2, value2, key3, value3): + assert len(map) == 3 + assert set(map.keys()) == {key1, key2, key3} + assert list(map.values()) == [value1, value2, value3] + assert set(map.items()) == {(key1, value1), (key2, value2), (key3, value3)} + assert map[key1] == value1 + assert map[key2] == value2 + assert map[key3] == value3 + with pytest.raises(KeyError): + _ = map[key1 + "blah"] + assert map == {key1: value1, key2: value2, key3: value3} + assert map == ArrayMap.empty().plus(key1, value1).plus(key2, value2).plus(key3, value3) - def test_empty(self): - self.assertEmpty(ArrayMap.empty()) +def test_empty(): + assertEmpty(ArrayMap.empty()) - def test_single(self): - empty = ArrayMap.empty() - single = empty.plus("one", "1") - self.assertEmpty(empty) - self.assertSingle(single, "one", "1") +def test_single(): + empty = ArrayMap.empty() + single = empty.plus("one", "1") + assertEmpty(empty) + assertSingle(single, "one", "1") - def test_double(self): - empty = ArrayMap.empty() - single = empty.plus("one", "1") - double = single.plus("two", "2") - self.assertEmpty(empty) - self.assertSingle(single, "one", "1") - self.assertDouble(double, "one", "1", "two", "2") - self.assertDouble(single.plus("a", "sorted"), "a", "sorted", "one", "1") +def test_double(): + empty = ArrayMap.empty() + single = empty.plus("one", "1") + double = single.plus("two", "2") + assertEmpty(empty) + assertSingle(single, "one", "1") + assertDouble(double, "one", "1", "two", "2") + assertDouble(single.plus("a", "sorted"), "a", "sorted", "one", "1") - with self.assertRaises(ValueError) as context: - single.plus("one", "2") - self.assertEqual(str(context.exception), "Key already exists") + with pytest.raises(ValueError) as context: + single.plus("one", "2") + assert str(context.value) == "Key already exists" - def test_triple(self): - triple = ArrayMap.empty().plus("1", "one").plus("2", "two").plus("3", "three") - self.assertTriple(triple, "1", "one", "2", "two", "3", "three") +def test_triple(): + triple = ArrayMap.empty().plus("1", "one").plus("2", "two").plus("3", "three") + assertTriple(triple, "1", "one", "2", "two", "3", "three") - def test_multi(self): - self.test_triple() - triple = ArrayMap.empty().plus("2", "two").plus("3", "three").plus("1", "one") - self.assertTriple(triple, "1", "one", "2", "two", "3", "three") - triple = ArrayMap.empty().plus("3", "three").plus("1", "one").plus("2", "two") - self.assertTriple(triple, "1", "one", "2", "two", "3", "three") - -if __name__ == '__main__': - unittest.main() +def test_multi(): + test_triple() # Calling another test function directly is unusual but works + triple = ArrayMap.empty().plus("2", "two").plus("3", "three").plus("1", "one") + assertTriple(triple, "1", "one", "2", "two", "3", "three") + triple = ArrayMap.empty().plus("3", "three").plus("1", "one").plus("2", "two") + assertTriple(triple, "1", "one", "2", "two", "3", "three") From 7b84b64bb34838bca21925ab78545db35f336765 Mon Sep 17 00:00:00 2001 From: Harvir Sahota Date: Mon, 26 Feb 2024 14:21:36 -0800 Subject: [PATCH 09/73] Use List[k] --- python/selfie-lib/selfie_lib/ArrayMap.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/python/selfie-lib/selfie_lib/ArrayMap.py b/python/selfie-lib/selfie_lib/ArrayMap.py index ddcb31a4..1d158163 100644 --- a/python/selfie-lib/selfie_lib/ArrayMap.py +++ b/python/selfie-lib/selfie_lib/ArrayMap.py @@ -3,8 +3,14 @@ from functools import cmp_to_key T = TypeVar('T') -K = TypeVar('K') V = TypeVar('V') +K = TypeVar('K', bound='Comparable') + +class Comparable: + def __lt__(self, other: 'Comparable') -> bool: ... + def __le__(self, other: 'Comparable') -> bool: ... + def __gt__(self, other: 'Comparable') -> bool: ... + def __ge__(self, other: 'Comparable') -> bool: ... class ListBackedSet(Set[T], Sequence[T]): def __init__(self): @@ -34,7 +40,7 @@ def __contains__(self, item: object) -> bool: return item in self._list class ArraySet(ListBackedSet[K]): - def __init__(self, data: list): + def __init__(self, data: List[K]): self.data = data self.sort_data() From e2b1a27af90d238508dca5eb5ed37159e442c358 Mon Sep 17 00:00:00 2001 From: Harvir Sahota Date: Mon, 26 Feb 2024 14:29:44 -0800 Subject: [PATCH 10/73] Add more tests for ArrayMap --- python/selfie-lib/tests/ArrayMap_test.py | 42 ++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/python/selfie-lib/tests/ArrayMap_test.py b/python/selfie-lib/tests/ArrayMap_test.py index 9d9550ae..f3f078e8 100644 --- a/python/selfie-lib/tests/ArrayMap_test.py +++ b/python/selfie-lib/tests/ArrayMap_test.py @@ -81,3 +81,45 @@ def test_multi(): assertTriple(triple, "1", "one", "2", "two", "3", "three") triple = ArrayMap.empty().plus("3", "three").plus("1", "one").plus("2", "two") assertTriple(triple, "1", "one", "2", "two", "3", "three") + +def test_minus_sorted_indices(): + initial_map = ArrayMap.empty().plus("1", "one").plus("2", "two").plus("3", "three").plus("4", "four") + modified_map = initial_map.minus_sorted_indices([1, 3]) + assert len(modified_map) == 2 + assert list(modified_map.keys()) == ["1", "3"] + assert list(modified_map.values()) == ["one", "three"] + with pytest.raises(KeyError): + _ = modified_map["2"] + with pytest.raises(KeyError): + _ = modified_map["4"] + assert modified_map == {"1": "one", "3": "three"} + +def test_plus_with_existing_keys(): + map_with_duplicates = ArrayMap.empty().plus("a", "alpha").plus("b", "beta") + with pytest.raises(ValueError): + map_with_duplicates.plus("a", "new alpha") + updated_map = map_with_duplicates.plus("c", "gamma") + assert len(updated_map) == 3 + assert updated_map["a"] == "alpha" + assert updated_map["b"] == "beta" + assert updated_map["c"] == "gamma" + modified_map = map_with_duplicates.minus_sorted_indices([0]).plus("a", "updated alpha") + assert len(modified_map) == 2 + assert modified_map["a"] == "updated alpha" + assert modified_map["b"] == "beta" + +def test_map_length(): + map = ArrayMap.empty() + assert len(map) == 0, "Length should be 0 for an empty map" + map = map.plus("key1", "value1") + assert len(map) == 1, "Length should be 1 after adding one item" + map = map.plus("key2", "value2") + assert len(map) == 2, "Length should be 2 after adding another item" + map = map.plus("key3", "value3") + assert len(map) == 3, "Length should be 3 after adding a third item" + map = map.minus_sorted_indices([1]) + assert len(map) == 2, "Length should be 2 after removing one item" + map = map.minus_sorted_indices([0]) + assert len(map) == 1, "Length should be 1 after removing another item" + map = map.minus_sorted_indices([0]) + assert len(map) == 0, "Length should be 0 after removing all items" From 888f072cbab24218f3c320835d08d3c3b41b2789 Mon Sep 17 00:00:00 2001 From: Edwin Date: Tue, 27 Feb 2024 10:17:18 -0800 Subject: [PATCH 11/73] Line Reader Translation --- python/selfie-lib/selfie_lib/LineReader.py | 75 ++++++++++------------ 1 file changed, 35 insertions(+), 40 deletions(-) diff --git a/python/selfie-lib/selfie_lib/LineReader.py b/python/selfie-lib/selfie_lib/LineReader.py index 154ae910..d2bc4c29 100644 --- a/python/selfie-lib/selfie_lib/LineReader.py +++ b/python/selfie-lib/selfie_lib/LineReader.py @@ -1,48 +1,43 @@ +from typing import Optional, Union import io -class LineTerminatorAware: - def __init__(self, reader): - self.reader = reader - self.line_number = 0 - self.unix_newlines = True - self.first_line = self.reader.readline() - if '\r' in self.first_line: - self.unix_newlines = False - - def read_line(self): - if self.first_line: - line = self.first_line - self.first_line = None - self.line_number += 1 - return line - line = self.reader.readline() +class LineReader: + """A line reader that is aware of line numbers and can detect Unix-style newlines.""" + + def __init__(self, source: Union[str, bytes]) -> None: + # Initialize the reader based on the type of source (string or bytes) + if isinstance(source, bytes): + self._reader = io.BufferedReader(io.BytesIO(source)) + else: + self._reader = io.StringIO(source) + self._line_number = 0 + self._unix_newlines = True + + def read_line(self) -> Optional[str]: + """Reads the next line from the source.""" + line = self._reader.readline() if line: + self._line_number += 1 + # Check for Unix newlines (only '\n' should be present) if '\r' in line: - self.unix_newlines = False - self.line_number += 1 - return line - - def unix_newlines(self): - return self.unix_newlines - - -class LineReader: - def __init__(self, reader): - self.reader = LineTerminatorAware(reader) - - @classmethod - def for_string(cls, content): - return cls(io.StringIO(content)) + self._unix_newlines = False + return line + return None - @classmethod - def for_binary(cls, content): - return cls(io.BytesIO(content).read().decode('utf-8')) + def get_line_number(self): # type: () -> int + """Returns the current line number.""" + return self._line_number - def get_line_number(self): - return self.reader.line_number + def unix_newlines(self): # type: () -> bool + """Checks if the read lines contain only Unix-style newlines.""" + return self._unix_newlines - def read_line(self): - return self.reader.read_line() + @staticmethod + def for_string(content: str): # type: (str) -> LineReader + """Creates a LineReader for a string.""" + return LineReader(content) - def unix_newlines(self): - return self.reader.unix_newlines() \ No newline at end of file + @staticmethod + def for_binary(content: bytes): # type: (bytes) -> LineReader + """Creates a LineReader for binary content.""" + return LineReader(content) From 74a22fb09eb1b69fbff22cc0cb7444d22532a517 Mon Sep 17 00:00:00 2001 From: Edwin Date: Tue, 27 Feb 2024 10:26:33 -0800 Subject: [PATCH 12/73] importing LineReader at init --- python/selfie-lib/selfie_lib/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/python/selfie-lib/selfie_lib/__init__.py b/python/selfie-lib/selfie_lib/__init__.py index 37e16097..5ffc0c82 100644 --- a/python/selfie-lib/selfie_lib/__init__.py +++ b/python/selfie-lib/selfie_lib/__init__.py @@ -1,5 +1,4 @@ from .ned import fizzbuzz as fizzbuzz from .harvir import silly_addition as silly_addition from .edwin import simple_subtraction as simple_subtraction -from .LineReader import LineReader as LineReader - +from .LineReader import LineReader as LineReader \ No newline at end of file From 4881e0f1e9546440bef0836c19b8d09031a239ed Mon Sep 17 00:00:00 2001 From: Edwin Date: Tue, 27 Feb 2024 10:31:08 -0800 Subject: [PATCH 13/73] Make changes to LineReader --- python/selfie-lib/selfie_lib/LineReader.py | 49 +++++++++------------- 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/python/selfie-lib/selfie_lib/LineReader.py b/python/selfie-lib/selfie_lib/LineReader.py index d2bc4c29..b9e8b602 100644 --- a/python/selfie-lib/selfie_lib/LineReader.py +++ b/python/selfie-lib/selfie_lib/LineReader.py @@ -2,42 +2,33 @@ import io class LineReader: - """A line reader that is aware of line numbers and can detect Unix-style newlines.""" - - def __init__(self, source: Union[str, bytes]) -> None: - # Initialize the reader based on the type of source (string or bytes) + def __init__(self, source: Union[bytes, str]): # Handling both binary and text input if isinstance(source, bytes): self._reader = io.BufferedReader(io.BytesIO(source)) - else: + else: # It's already a str, so we use StringIO for text self._reader = io.StringIO(source) - self._line_number = 0 self._unix_newlines = True - def read_line(self) -> Optional[str]: - """Reads the next line from the source.""" - line = self._reader.readline() - if line: - self._line_number += 1 - # Check for Unix newlines (only '\n' should be present) - if '\r' in line: - self._unix_newlines = False - return line - return None - - def get_line_number(self): # type: () -> int - """Returns the current line number.""" - return self._line_number - - def unix_newlines(self): # type: () -> bool - """Checks if the read lines contain only Unix-style newlines.""" - return self._unix_newlines - @staticmethod - def for_string(content: str): # type: (str) -> LineReader - """Creates a LineReader for a string.""" + def for_binary(content: bytes) -> 'LineReader': + """Creates a LineReader for binary content.""" return LineReader(content) @staticmethod - def for_binary(content: bytes): # type: (bytes) -> LineReader - """Creates a LineReader for binary content.""" + def for_string(content: str) -> 'LineReader': + """Creates a LineReader for string content.""" return LineReader(content) + + def read_line(self) -> Optional[str]: + line = self._reader.readline() + # Check and convert bytes to str if necessary + if isinstance(line, bytes): + line = line.decode('utf-8') + if line == '': + return None # EOF + if '\r' in line: + self._unix_newlines = False + return line.rstrip('\n') + + def unix_newlines(self) -> bool: + return self._unix_newlines From 2427d972a12386b4975e4e45493750716846c2ef Mon Sep 17 00:00:00 2001 From: Edwin Date: Tue, 27 Feb 2024 10:39:13 -0800 Subject: [PATCH 14/73] Make changes to LineReader --- python/selfie-lib/selfie_lib/LineReader.py | 69 +++++++++++++++------- 1 file changed, 47 insertions(+), 22 deletions(-) diff --git a/python/selfie-lib/selfie_lib/LineReader.py b/python/selfie-lib/selfie_lib/LineReader.py index b9e8b602..31ced7af 100644 --- a/python/selfie-lib/selfie_lib/LineReader.py +++ b/python/selfie-lib/selfie_lib/LineReader.py @@ -1,34 +1,59 @@ from typing import Optional, Union import io -class LineReader: - def __init__(self, source: Union[bytes, str]): # Handling both binary and text input - if isinstance(source, bytes): - self._reader = io.BufferedReader(io.BytesIO(source)) - else: # It's already a str, so we use StringIO for text - self._reader = io.StringIO(source) +class LineTerminatorReader(io.BufferedReader): + """Overrides read operations to detect carriage returns.""" + def __init__(self, reader: io.TextIOWrapper) -> None: + super().__init__(reader.buffer) self._unix_newlines = True - @staticmethod - def for_binary(content: bytes) -> 'LineReader': - """Creates a LineReader for binary content.""" - return LineReader(content) + def read(self, size: int = -1) -> bytes: + chunk = super().read(size) + if b'\r' in chunk: + self._unix_newlines = False + return chunk + + def unix_newlines(self) -> bool: + """Check if the newlines are Unix style.""" + return self._unix_newlines + +class LineTerminatorAware(io.TextIOWrapper): + """Keeps track of the first line to determine newline style.""" + def __init__(self, reader: LineTerminatorReader) -> None: + super().__init__(reader, encoding='utf-8') + self._first_line: Optional[str] = self.readline() + + def readline(self, limit: int = -1) -> str: + if self._first_line is not None: + result, self._first_line = self._first_line, None + return result + return super().readline(limit) + +class LineReader: + """A reader that is aware of line terminators and line numbers.""" + def __init__(self, reader: Union[io.StringIO, io.BufferedReader]) -> None: + self._reader = LineTerminatorAware(LineTerminatorReader(reader)) @staticmethod def for_string(content: str) -> 'LineReader': - """Creates a LineReader for string content.""" - return LineReader(content) + """Create a LineReader for string content.""" + return LineReader(io.StringIO(content)) + + @staticmethod + def for_binary(content: bytes) -> 'LineReader': + """Create a LineReader for binary content.""" + return LineReader(io.BufferedReader(io.BytesIO(content))) + + def get_line_number(self) -> int: + """Get the current line number.""" + # Assuming a way to track line numbers or using a wrapper that does. + # This is a placeholder as Python's io does not provide a direct lineno attribute. + return 0 def read_line(self) -> Optional[str]: - line = self._reader.readline() - # Check and convert bytes to str if necessary - if isinstance(line, bytes): - line = line.decode('utf-8') - if line == '': - return None # EOF - if '\r' in line: - self._unix_newlines = False - return line.rstrip('\n') + """Read the next line from the reader.""" + return self._reader.readline() def unix_newlines(self) -> bool: - return self._unix_newlines + """Check if the reader uses Unix newlines.""" + return self._reader.unix_newlines() From 419a64b2b010ec3c559021c7123a057d719604cc Mon Sep 17 00:00:00 2001 From: Edwin Date: Tue, 27 Feb 2024 10:51:48 -0800 Subject: [PATCH 15/73] Make changes to linereader test --- python/selfie-lib/tests/lineReaderTest.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/python/selfie-lib/tests/lineReaderTest.py b/python/selfie-lib/tests/lineReaderTest.py index 167ad398..d713e1d0 100644 --- a/python/selfie-lib/tests/lineReaderTest.py +++ b/python/selfie-lib/tests/lineReaderTest.py @@ -1,8 +1,5 @@ from selfie_lib import LineReader -# Assuming the LineReader class is defined as previously translated -# Adjustments may be needed if the LineReader implementation differs - def test_should_find_unix_separator_from_binary(): reader = LineReader.for_binary(b"This is a new line\n") assert reader.unix_newlines() == True @@ -20,12 +17,12 @@ def test_should_find_unix_separator_from_string(): def test_should_find_windows_separator_from_string(): reader = LineReader.for_string("This is a new line\r\n") - assert reader.unix_newlines() is False + assert reader.unix_newlines() == False assert reader.read_line() == "This is a new line" def test_should_get_unix_line_separator_when_there_is_none(): reader = LineReader.for_binary(b"This is a new line") - assert reader.unix_newlines() is True + assert reader.unix_newlines() == True assert reader.read_line() == "This is a new line" def test_should_read_next_line_without_problem(): From ef49c27ea25df93f1831a0cbbb00eeb947e264df Mon Sep 17 00:00:00 2001 From: Edwin Date: Tue, 27 Feb 2024 11:01:10 -0800 Subject: [PATCH 16/73] Make changes to LineReader --- python/selfie-lib/selfie_lib/LineReader.py | 97 +++++++++++----------- 1 file changed, 48 insertions(+), 49 deletions(-) diff --git a/python/selfie-lib/selfie_lib/LineReader.py b/python/selfie-lib/selfie_lib/LineReader.py index 31ced7af..d649713c 100644 --- a/python/selfie-lib/selfie_lib/LineReader.py +++ b/python/selfie-lib/selfie_lib/LineReader.py @@ -1,59 +1,58 @@ from typing import Optional, Union -import io - -class LineTerminatorReader(io.BufferedReader): - """Overrides read operations to detect carriage returns.""" - def __init__(self, reader: io.TextIOWrapper) -> None: - super().__init__(reader.buffer) - self._unix_newlines = True - - def read(self, size: int = -1) -> bytes: - chunk = super().read(size) - if b'\r' in chunk: - self._unix_newlines = False +from io import TextIOWrapper, StringIO, BytesIO, BufferedReader + +class LineTerminatorReader: + """ + Reads from an underlying buffer and detects Unix-style newlines. + """ + def __init__(self, reader: Union[TextIOWrapper, BufferedReader, StringIO, BytesIO]) -> None: + # Wrap the reader appropriately based on its type + if isinstance(reader, (StringIO, BytesIO)): + self.reader: Union[StringIO, BytesIO] = reader + elif isinstance(reader, (TextIOWrapper, BufferedReader)): + self.reader = reader + else: + raise ValueError("Unsupported reader type") + self.unix_newlines: bool = True + + def read(self, size: int = -1) -> str: + chunk: str = self.reader.read(size) + if '\r' in chunk: + self.unix_newlines = False return chunk - def unix_newlines(self) -> bool: - """Check if the newlines are Unix style.""" - return self._unix_newlines - -class LineTerminatorAware(io.TextIOWrapper): - """Keeps track of the first line to determine newline style.""" - def __init__(self, reader: LineTerminatorReader) -> None: - super().__init__(reader, encoding='utf-8') - self._first_line: Optional[str] = self.readline() + def readline(self) -> Optional[str]: + line: str = self.reader.readline() + if '\r' in line: + self.unix_newlines = False + return line.strip('\r\n') - def readline(self, limit: int = -1) -> str: - if self._first_line is not None: - result, self._first_line = self._first_line, None - return result - return super().readline(limit) + def unix_newlines(self) -> bool: + return self.unix_newlines class LineReader: - """A reader that is aware of line terminators and line numbers.""" - def __init__(self, reader: Union[io.StringIO, io.BufferedReader]) -> None: - self._reader = LineTerminatorAware(LineTerminatorReader(reader)) - - @staticmethod - def for_string(content: str) -> 'LineReader': - """Create a LineReader for string content.""" - return LineReader(io.StringIO(content)) - - @staticmethod - def for_binary(content: bytes) -> 'LineReader': - """Create a LineReader for binary content.""" - return LineReader(io.BufferedReader(io.BytesIO(content))) - - def get_line_number(self) -> int: - """Get the current line number.""" - # Assuming a way to track line numbers or using a wrapper that does. - # This is a placeholder as Python's io does not provide a direct lineno attribute. - return 0 + """ + Facilitates reading lines from a string or binary content, detecting newline style. + """ + def __init__(self, source: Union[str, bytes]) -> None: + if isinstance(source, str): + reader = StringIO(source) + elif isinstance(source, bytes): + reader = BytesIO(source) + else: + raise ValueError("Source must be either 'str' or 'bytes'.") + self.terminator_reader: LineTerminatorReader = LineTerminatorReader(reader) + + @classmethod + def for_string(cls, content: str) -> 'LineReader': + return cls(content) + + @classmethod + def for_binary(cls, content: bytes) -> 'LineReader': + return cls(content) def read_line(self) -> Optional[str]: - """Read the next line from the reader.""" - return self._reader.readline() + return self.terminator_reader.readline() def unix_newlines(self) -> bool: - """Check if the reader uses Unix newlines.""" - return self._reader.unix_newlines() + return self.terminator_reader.unix_newlines() From 013a094687fbc2e2d9fb68345b58b36d4955e458 Mon Sep 17 00:00:00 2001 From: Edwin Date: Tue, 27 Feb 2024 11:15:55 -0800 Subject: [PATCH 17/73] Make changes to the errors --- python/selfie-lib/tests/lineReaderTest.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/python/selfie-lib/tests/lineReaderTest.py b/python/selfie-lib/tests/lineReaderTest.py index d713e1d0..f42b9308 100644 --- a/python/selfie-lib/tests/lineReaderTest.py +++ b/python/selfie-lib/tests/lineReaderTest.py @@ -2,41 +2,41 @@ def test_should_find_unix_separator_from_binary(): reader = LineReader.for_binary(b"This is a new line\n") - assert reader.unix_newlines() == True + assert reader.unix_newlines() is True assert reader.read_line() == "This is a new line" def test_should_find_windows_separator_from_binary(): reader = LineReader.for_binary(b"This is a new line\r\n") - assert reader.unix_newlines() == False + assert reader.unix_newlines() is False assert reader.read_line() == "This is a new line" def test_should_find_unix_separator_from_string(): reader = LineReader.for_string("This is a new line\n") - assert reader.unix_newlines() == True + assert reader.unix_newlines() is True assert reader.read_line() == "This is a new line" def test_should_find_windows_separator_from_string(): reader = LineReader.for_string("This is a new line\r\n") - assert reader.unix_newlines() == False + assert reader.unix_newlines() is False assert reader.read_line() == "This is a new line" def test_should_get_unix_line_separator_when_there_is_none(): reader = LineReader.for_binary(b"This is a new line") - assert reader.unix_newlines() == True + assert reader.unix_newlines() is True assert reader.read_line() == "This is a new line" def test_should_read_next_line_without_problem(): reader = LineReader.for_binary(b"First\r\nSecond\r\n") - assert reader.unix_newlines() == False + assert reader.unix_newlines() is False assert reader.read_line() == "First" - assert reader.unix_newlines() == False + assert reader.unix_newlines() is False assert reader.read_line() == "Second" - assert reader.unix_newlines() == False + assert reader.unix_newlines() is False def test_should_use_first_line_separator_and_ignore_next(): reader = LineReader.for_binary(b"First\r\nAnother separator\n") - assert reader.unix_newlines() == False + assert reader.unix_newlines() is False assert reader.read_line() == "First" - assert reader.unix_newlines() == False + assert reader.unix_newlines() is False assert reader.read_line() == "Another separator" - assert reader.unix_newlines() == False + assert reader.unix_newlines() is False From c78c42471d3baef07f4f01fb419a83db56347b49 Mon Sep 17 00:00:00 2001 From: Edwin Date: Tue, 27 Feb 2024 11:33:24 -0800 Subject: [PATCH 18/73] Make change LineReader --- python/selfie-lib/selfie_lib/LineReader.py | 92 +++++++++++----------- 1 file changed, 44 insertions(+), 48 deletions(-) diff --git a/python/selfie-lib/selfie_lib/LineReader.py b/python/selfie-lib/selfie_lib/LineReader.py index d649713c..be49917d 100644 --- a/python/selfie-lib/selfie_lib/LineReader.py +++ b/python/selfie-lib/selfie_lib/LineReader.py @@ -1,58 +1,54 @@ -from typing import Optional, Union -from io import TextIOWrapper, StringIO, BytesIO, BufferedReader - -class LineTerminatorReader: - """ - Reads from an underlying buffer and detects Unix-style newlines. - """ - def __init__(self, reader: Union[TextIOWrapper, BufferedReader, StringIO, BytesIO]) -> None: - # Wrap the reader appropriately based on its type - if isinstance(reader, (StringIO, BytesIO)): - self.reader: Union[StringIO, BytesIO] = reader - elif isinstance(reader, (TextIOWrapper, BufferedReader)): - self.reader = reader - else: - raise ValueError("Unsupported reader type") - self.unix_newlines: bool = True - - def read(self, size: int = -1) -> str: - chunk: str = self.reader.read(size) - if '\r' in chunk: - self.unix_newlines = False - return chunk +import io - def readline(self) -> Optional[str]: - line: str = self.reader.readline() - if '\r' in line: - self.unix_newlines = False - return line.strip('\r\n') +class LineTerminatorAware(io.BufferedReader): + def __init__(self, buffer): + super().__init__(buffer) + self.first_line = self.readline() + self.unix_newlines = True - def unix_newlines(self) -> bool: - return self.unix_newlines + def readline(self, *args, **kwargs): + if self.first_line is not None: + result = self.first_line + self.first_line = None + return result + return super().readline(*args, **kwargs) class LineReader: - """ - Facilitates reading lines from a string or binary content, detecting newline style. - """ - def __init__(self, source: Union[str, bytes]) -> None: - if isinstance(source, str): - reader = StringIO(source) - elif isinstance(source, bytes): - reader = BytesIO(source) - else: - raise ValueError("Source must be either 'str' or 'bytes'.") - self.terminator_reader: LineTerminatorReader = LineTerminatorReader(reader) + def __init__(self, reader): + self.reader = LineTerminatorAware(LineTerminatorReader(reader)) @classmethod - def for_string(cls, content: str) -> 'LineReader': - return cls(content) + def for_string(cls, content): + return cls(io.StringIO(content)) @classmethod - def for_binary(cls, content: bytes) -> 'LineReader': - return cls(content) + def for_binary(cls, content): + return cls(io.BytesIO(content).read().decode('utf-8')) + + def get_line_number(self): + # Python's io module does not track line numbers directly. + # This functionality would need to be implemented manually if required. + pass + + def read_line(self): + return self.reader.readline() - def read_line(self) -> Optional[str]: - return self.terminator_reader.readline() + def unix_newlines(self): + return self.reader.unix_newlines - def unix_newlines(self) -> bool: - return self.terminator_reader.unix_newlines() +class LineTerminatorReader(io.StringIO): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.unix_newlines = True + + def read(self, *args, **kwargs): + result = super().read(*args, **kwargs) + if '\r' in result: + self.unix_newlines = False + return result + + def readline(self, *args, **kwargs): + line = super().readline(*args, **kwargs) + if '\r' in line: + self.unix_newlines = False + return line From 4596ba9c01830e5d7dc7de4f625a44d5da21de35 Mon Sep 17 00:00:00 2001 From: Edwin Date: Tue, 27 Feb 2024 14:10:49 -0800 Subject: [PATCH 19/73] Make changes to LineReader and rename LineReader_test --- python/selfie-lib/selfie_lib/LineReader.py | 72 +++++++------------ python/selfie-lib/selfie_lib/__init__.py | 4 -- .../{lineReaderTest.py => LineReader_test.py} | 0 3 files changed, 25 insertions(+), 51 deletions(-) rename python/selfie-lib/tests/{lineReaderTest.py => LineReader_test.py} (100%) diff --git a/python/selfie-lib/selfie_lib/LineReader.py b/python/selfie-lib/selfie_lib/LineReader.py index be49917d..b2aa8c2b 100644 --- a/python/selfie-lib/selfie_lib/LineReader.py +++ b/python/selfie-lib/selfie_lib/LineReader.py @@ -1,54 +1,32 @@ import io - -class LineTerminatorAware(io.BufferedReader): - def __init__(self, buffer): - super().__init__(buffer) - self.first_line = self.readline() - self.unix_newlines = True - - def readline(self, *args, **kwargs): - if self.first_line is not None: - result = self.first_line - self.first_line = None - return result - return super().readline(*args, **kwargs) +from typing import Union class LineReader: - def __init__(self, reader): - self.reader = LineTerminatorAware(LineTerminatorReader(reader)) + def __init__(self, content: Union[bytes, str]): + if isinstance(content, str): + content = content.encode('utf-8') + self.buffer = io.BytesIO(content) + self.uses_unix_newlines = self.detect_newline_type() + + def detect_newline_type(self) -> bool: + first_line = self.buffer.readline() + # Reset buffer after checking the first line + self.buffer.seek(0) + return b'\r\n' not in first_line @classmethod - def for_string(cls, content): - return cls(io.StringIO(content)) + def for_binary(cls, content: bytes): + return cls(content) @classmethod - def for_binary(cls, content): - return cls(io.BytesIO(content).read().decode('utf-8')) - - def get_line_number(self): - # Python's io module does not track line numbers directly. - # This functionality would need to be implemented manually if required. - pass - - def read_line(self): - return self.reader.readline() - - def unix_newlines(self): - return self.reader.unix_newlines - -class LineTerminatorReader(io.StringIO): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.unix_newlines = True - - def read(self, *args, **kwargs): - result = super().read(*args, **kwargs) - if '\r' in result: - self.unix_newlines = False - return result - - def readline(self, *args, **kwargs): - line = super().readline(*args, **kwargs) - if '\r' in line: - self.unix_newlines = False - return line + def for_string(cls, content: str): + return cls(content) + + def unix_newlines(self) -> bool: + return self.uses_unix_newlines + + def read_line(self) -> str: + line = self.buffer.readline().decode('utf-8') + return line.rstrip('\r\n') + + diff --git a/python/selfie-lib/selfie_lib/__init__.py b/python/selfie-lib/selfie_lib/__init__.py index 4bcf28f3..e2859a5d 100644 --- a/python/selfie-lib/selfie_lib/__init__.py +++ b/python/selfie-lib/selfie_lib/__init__.py @@ -1,7 +1,3 @@ -from .ned import fizzbuzz as fizzbuzz -from .selina import get_interesting_fact as get_interesting_fact -from .harvir import silly_addition as silly_addition -from .edwin import simple_subtraction as simple_subtraction from .LineReader import LineReader as LineReader from .Slice import Slice as Slice diff --git a/python/selfie-lib/tests/lineReaderTest.py b/python/selfie-lib/tests/LineReader_test.py similarity index 100% rename from python/selfie-lib/tests/lineReaderTest.py rename to python/selfie-lib/tests/LineReader_test.py From 94be139383d4014fa37c651d93feaec91e16b526 Mon Sep 17 00:00:00 2001 From: Harvir Sahota Date: Tue, 27 Feb 2024 15:36:08 -0800 Subject: [PATCH 20/73] Make methods private --- python/selfie-lib/selfie_lib/ArrayMap.py | 34 ++++++++++++------------ 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/python/selfie-lib/selfie_lib/ArrayMap.py b/python/selfie-lib/selfie_lib/ArrayMap.py index 1d158163..e1b34f1e 100644 --- a/python/selfie-lib/selfie_lib/ArrayMap.py +++ b/python/selfie-lib/selfie_lib/ArrayMap.py @@ -41,16 +41,16 @@ def __contains__(self, item: object) -> bool: class ArraySet(ListBackedSet[K]): def __init__(self, data: List[K]): - self.data = data - self.sort_data() + self.__data = data + self.__sort_data() - def sort_data(self): - if self.data and isinstance(self.data[0], str): - self.data.sort(key=cmp_to_key(self.string_slash_first_comparator)) + def __sort_data(self): + if self.__data and isinstance(self.__data[0], str): + self.__data.sort(key=cmp_to_key(self.__string_slash_first_comparator)) else: - self.data.sort() + self.__data.sort() - def string_slash_first_comparator(self, a, b): + def __string_slash_first_comparator(self, a, b): if a == '/': return -1 elif b == '/': @@ -59,7 +59,7 @@ def string_slash_first_comparator(self, a, b): return (a > b) - (a < b) def __len__(self): - return len(self.data) + return len(self.__data) @overload def __getitem__(self, index: int) -> K: ... @@ -69,30 +69,30 @@ def __getitem__(self, index: slice) -> 'ArraySet[K]': ... def __getitem__(self, index: Union[int, slice]) -> Union[K, 'ArraySet[K]']: if isinstance(index, int): - return self.data[index] + return self.__data[index] elif isinstance(index, slice): - sliced_data = self.data[index] + sliced_data = self.__data[index] return ArraySet(sliced_data) else: raise TypeError("Index must be int or slice") def __contains__(self, item: object) -> bool: - if not isinstance(item, type(self.data[0])): + if not isinstance(item, type(self.__data[0])): return False - return super().__contains__(item) + return item in self.__data def plus_or_this(self, key: K) -> 'ArraySet[K]': - left, right = 0, len(self.data) - 1 + left, right = 0, len(self.__data) - 1 while left <= right: mid = (left + right) // 2 - if self.data[mid] == key: - return self - elif self.data[mid] < key: + if self.__data[mid] == key: + return self + elif self.__data[mid] < key: left = mid + 1 else: right = mid - 1 - new_data = self.data[:left] + [key] + self.data[left:] + new_data = self.__data[:left] + [key] + self.__data[left:] return ArraySet(new_data) class ArrayMap(Mapping[K, V]): From 87625b2112b65b0169ccd4a8a67b8119bcd69f48 Mon Sep 17 00:00:00 2001 From: Harvir Sahota Date: Tue, 27 Feb 2024 15:42:11 -0800 Subject: [PATCH 21/73] Implement method in terms of .__data --- python/selfie-lib/selfie_lib/ArrayMap.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python/selfie-lib/selfie_lib/ArrayMap.py b/python/selfie-lib/selfie_lib/ArrayMap.py index e1b34f1e..8204d571 100644 --- a/python/selfie-lib/selfie_lib/ArrayMap.py +++ b/python/selfie-lib/selfie_lib/ArrayMap.py @@ -62,7 +62,8 @@ def __len__(self): return len(self.__data) @overload - def __getitem__(self, index: int) -> K: ... + def __getitem__(self, index: int) -> K: + return self.__data[index] @overload def __getitem__(self, index: slice) -> 'ArraySet[K]': ... From 66aa871cf0f6c753043dbb28556f38f9980d810d Mon Sep 17 00:00:00 2001 From: Edwin Date: Tue, 27 Feb 2024 16:00:35 -0800 Subject: [PATCH 22/73] Make detect_newline_type private --- python/selfie-lib/selfie_lib/LineReader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/selfie-lib/selfie_lib/LineReader.py b/python/selfie-lib/selfie_lib/LineReader.py index b2aa8c2b..4aa80157 100644 --- a/python/selfie-lib/selfie_lib/LineReader.py +++ b/python/selfie-lib/selfie_lib/LineReader.py @@ -8,7 +8,7 @@ def __init__(self, content: Union[bytes, str]): self.buffer = io.BytesIO(content) self.uses_unix_newlines = self.detect_newline_type() - def detect_newline_type(self) -> bool: + def __detect_newline_type(self) -> bool: first_line = self.buffer.readline() # Reset buffer after checking the first line self.buffer.seek(0) From aab9d492d28e2fb8c6e8e3cb13843653f485a065 Mon Sep 17 00:00:00 2001 From: Edwin Date: Tue, 27 Feb 2024 16:05:25 -0800 Subject: [PATCH 23/73] Make changes to detect new line and add get line number --- python/selfie-lib/selfie_lib/LineReader.py | 38 ++++++++++++---------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/python/selfie-lib/selfie_lib/LineReader.py b/python/selfie-lib/selfie_lib/LineReader.py index 4aa80157..9c007236 100644 --- a/python/selfie-lib/selfie_lib/LineReader.py +++ b/python/selfie-lib/selfie_lib/LineReader.py @@ -1,32 +1,36 @@ import io -from typing import Union class LineReader: - def __init__(self, content: Union[bytes, str]): - if isinstance(content, str): - content = content.encode('utf-8') + def __init__(self, content: bytes): self.buffer = io.BytesIO(content) - self.uses_unix_newlines = self.detect_newline_type() - - def __detect_newline_type(self) -> bool: - first_line = self.buffer.readline() - # Reset buffer after checking the first line - self.buffer.seek(0) - return b'\r\n' not in first_line + # Making the method private as per the comment + self.uses_unix_newlines = self._detect_newline_type() + self.line_count = 0 # Initialize line count @classmethod def for_binary(cls, content: bytes): return cls(content) - + @classmethod def for_string(cls, content: str): - return cls(content) + return cls(content.encode('utf-8')) + + # Now a private method + def _detect_newline_type(self) -> bool: + first_line = self.buffer.readline() + self.buffer.seek(0) # Reset buffer for actual reading + return b'\r\n' not in first_line def unix_newlines(self) -> bool: return self.uses_unix_newlines def read_line(self) -> str: - line = self.buffer.readline().decode('utf-8') - return line.rstrip('\r\n') - - + line_bytes = self.buffer.readline() + if line_bytes: + self.line_count += 1 # Increment line count for each line read + line = line_bytes.decode('utf-8') + return line.rstrip('\r\n' if not self.uses_unix_newlines else '\n') + + # Method to get the current line number + def get_line_number(self) -> int: + return self.line_count From 257968ee9d766041c26e14a20064e9f781e63c1c Mon Sep 17 00:00:00 2001 From: Harvir Sahota Date: Tue, 27 Feb 2024 17:02:31 -0800 Subject: [PATCH 24/73] Update ListBackedSet and ArraySet --- python/selfie-lib/selfie_lib/ArrayMap.py | 96 +++++++----------------- 1 file changed, 27 insertions(+), 69 deletions(-) diff --git a/python/selfie-lib/selfie_lib/ArrayMap.py b/python/selfie-lib/selfie_lib/ArrayMap.py index 8204d571..b9b5b3c4 100644 --- a/python/selfie-lib/selfie_lib/ArrayMap.py +++ b/python/selfie-lib/selfie_lib/ArrayMap.py @@ -1,56 +1,47 @@ from collections.abc import Set, Sequence, Iterator, Mapping -from typing import List, TypeVar, Union, overload, Sequence as TypeSequence +from typing import List, TypeVar from functools import cmp_to_key +from abc import ABC, abstractmethod T = TypeVar('T') V = TypeVar('V') K = TypeVar('K', bound='Comparable') -class Comparable: +class Comparable(ABC): def __lt__(self, other: 'Comparable') -> bool: ... def __le__(self, other: 'Comparable') -> bool: ... def __gt__(self, other: 'Comparable') -> bool: ... def __ge__(self, other: 'Comparable') -> bool: ... -class ListBackedSet(Set[T], Sequence[T]): - def __init__(self): - self._list: List[T] = [] +class ListBackedSet(Set[T], Sequence[T], ABC): + @abstractmethod + def __len__(self) -> int: ... - @overload + @abstractmethod def __getitem__(self, index: int) -> T: ... - - @overload - def __getitem__(self, index: slice) -> TypeSequence[T]: ... - - def __getitem__(self, index: Union[int, slice]) -> Union[T, TypeSequence[T]]: - if isinstance(index, int): - return self._list[index] - elif isinstance(index, slice): - return self._list[index] - else: - raise TypeError("Index must be int or slice") - - def __len__(self) -> int: - return len(self._list) def __iter__(self) -> Iterator[T]: - return iter(self._list) + for i in range(len(self)): + yield self[i] def __contains__(self, item: object) -> bool: - return item in self._list + for i in range(len(self)): + if self[i] == item: + return True + return False class ArraySet(ListBackedSet[K]): def __init__(self, data: List[K]): - self.__data = data - self.__sort_data() + self.__data = data + self.__sort_data() def __sort_data(self): - if self.__data and isinstance(self.__data[0], str): + if self.__data and isinstance(self.__data[0], Comparable): self.__data.sort(key=cmp_to_key(self.__string_slash_first_comparator)) else: self.__data.sort() - def __string_slash_first_comparator(self, a, b): + def __string_slash_first_comparator(self, a: K, b: K) -> int: if a == '/': return -1 elif b == '/': @@ -58,44 +49,12 @@ def __string_slash_first_comparator(self, a, b): else: return (a > b) - (a < b) - def __len__(self): + def __len__(self) -> int: return len(self.__data) - @overload - def __getitem__(self, index: int) -> K: + def __getitem__(self, index: int) -> K: return self.__data[index] - @overload - def __getitem__(self, index: slice) -> 'ArraySet[K]': ... - - def __getitem__(self, index: Union[int, slice]) -> Union[K, 'ArraySet[K]']: - if isinstance(index, int): - return self.__data[index] - elif isinstance(index, slice): - sliced_data = self.__data[index] - return ArraySet(sliced_data) - else: - raise TypeError("Index must be int or slice") - - def __contains__(self, item: object) -> bool: - if not isinstance(item, type(self.__data[0])): - return False - return item in self.__data - - def plus_or_this(self, key: K) -> 'ArraySet[K]': - left, right = 0, len(self.__data) - 1 - while left <= right: - mid = (left + right) // 2 - if self.__data[mid] == key: - return self - elif self.__data[mid] < key: - left = mid + 1 - else: - right = mid - 1 - - new_data = self.__data[:left] + [key] + self.__data[left:] - return ArraySet(new_data) - class ArrayMap(Mapping[K, V]): def __init__(self, data: list): self.data = data @@ -103,21 +62,21 @@ def __init__(self, data: list): @classmethod def empty(cls): return cls([]) - + def __getitem__(self, key: K) -> V: index = self._binary_search_key(key) if index >= 0: return self.data[2 * index + 1] raise KeyError(key) - def __iter__(self): + def __iter__(self) -> Iterator[K]: return (self.data[i] for i in range(0, len(self.data), 2)) def __len__(self) -> int: return len(self.data) // 2 def _binary_search_key(self, key: K) -> int: - low, high = 0, len(self.data) // 2 - 1 + low, high = 0, (len(self.data) // 2) - 1 while low <= high: mid = (low + high) // 2 mid_key = self.data[2 * mid] @@ -133,13 +92,12 @@ def plus(self, key: K, value: V) -> 'ArrayMap[K, V]': index = self._binary_search_key(key) if index >= 0: raise ValueError("Key already exists") - else: - insert_at = -(index + 1) - new_data = self.data[:] - new_data[insert_at * 2:insert_at * 2] = [key, value] - return ArrayMap(new_data) + insert_at = -(index + 1) + new_data = self.data[:] + new_data[insert_at * 2:insert_at * 2] = [key, value] + return ArrayMap(new_data) - def minus_sorted_indices(self, indicesToRemove: list[int]) -> 'ArrayMap[K, V]': + def minus_sorted_indices(self, indicesToRemove: List[int]) -> 'ArrayMap[K, V]': if not indicesToRemove: return self newData = [] From 94e5e547016b3a9f65d00404e2294ed4a29fcd5d Mon Sep 17 00:00:00 2001 From: Harvir Sahota Date: Tue, 27 Feb 2024 17:50:04 -0800 Subject: [PATCH 25/73] Remove type bound and Comparable class --- python/selfie-lib/selfie_lib/ArrayMap.py | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/python/selfie-lib/selfie_lib/ArrayMap.py b/python/selfie-lib/selfie_lib/ArrayMap.py index b9b5b3c4..3946616b 100644 --- a/python/selfie-lib/selfie_lib/ArrayMap.py +++ b/python/selfie-lib/selfie_lib/ArrayMap.py @@ -1,17 +1,10 @@ from collections.abc import Set, Sequence, Iterator, Mapping from typing import List, TypeVar -from functools import cmp_to_key from abc import ABC, abstractmethod T = TypeVar('T') V = TypeVar('V') -K = TypeVar('K', bound='Comparable') - -class Comparable(ABC): - def __lt__(self, other: 'Comparable') -> bool: ... - def __le__(self, other: 'Comparable') -> bool: ... - def __gt__(self, other: 'Comparable') -> bool: ... - def __ge__(self, other: 'Comparable') -> bool: ... +K = TypeVar('K') class ListBackedSet(Set[T], Sequence[T], ABC): @abstractmethod @@ -36,18 +29,7 @@ def __init__(self, data: List[K]): self.__sort_data() def __sort_data(self): - if self.__data and isinstance(self.__data[0], Comparable): - self.__data.sort(key=cmp_to_key(self.__string_slash_first_comparator)) - else: - self.__data.sort() - - def __string_slash_first_comparator(self, a: K, b: K) -> int: - if a == '/': - return -1 - elif b == '/': - return 1 - else: - return (a > b) - (a < b) + self.__data.sort() def __len__(self) -> int: return len(self.__data) From b42617ae7f7cb5b32814a9b0a75926588535b526 Mon Sep 17 00:00:00 2001 From: Harvir Sahota Date: Tue, 27 Feb 2024 18:46:33 -0800 Subject: [PATCH 26/73] Update ArraySet --- python/selfie-lib/selfie_lib/ArrayMap.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/python/selfie-lib/selfie_lib/ArrayMap.py b/python/selfie-lib/selfie_lib/ArrayMap.py index 3946616b..f76a4cea 100644 --- a/python/selfie-lib/selfie_lib/ArrayMap.py +++ b/python/selfie-lib/selfie_lib/ArrayMap.py @@ -26,10 +26,6 @@ def __contains__(self, item: object) -> bool: class ArraySet(ListBackedSet[K]): def __init__(self, data: List[K]): self.__data = data - self.__sort_data() - - def __sort_data(self): - self.__data.sort() def __len__(self) -> int: return len(self.__data) From bae3364317183174ee5a937e5320787c4a983529 Mon Sep 17 00:00:00 2001 From: Harvir Sahota Date: Tue, 27 Feb 2024 19:16:55 -0800 Subject: [PATCH 27/73] Update __getitem__ --- python/selfie-lib/selfie_lib/ArrayMap.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/python/selfie-lib/selfie_lib/ArrayMap.py b/python/selfie-lib/selfie_lib/ArrayMap.py index f76a4cea..7d7ba570 100644 --- a/python/selfie-lib/selfie_lib/ArrayMap.py +++ b/python/selfie-lib/selfie_lib/ArrayMap.py @@ -1,21 +1,17 @@ from collections.abc import Set, Sequence, Iterator, Mapping -from typing import List, TypeVar -from abc import ABC, abstractmethod +from typing import List, TypeVar, Union +from abc import abstractmethod T = TypeVar('T') V = TypeVar('V') K = TypeVar('K') -class ListBackedSet(Set[T], Sequence[T], ABC): +class ListBackedSet(Set[T], Sequence[T]): @abstractmethod def __len__(self) -> int: ... @abstractmethod - def __getitem__(self, index: int) -> T: ... - - def __iter__(self) -> Iterator[T]: - for i in range(len(self)): - yield self[i] + def __getitem__(self, index: Union[int, slice]) -> Union[T, Sequence[T]]: ... def __contains__(self, item: object) -> bool: for i in range(len(self)): @@ -30,8 +26,13 @@ def __init__(self, data: List[K]): def __len__(self) -> int: return len(self.__data) - def __getitem__(self, index: int) -> K: - return self.__data[index] + def __getitem__(self, index: Union[int, slice]) -> Union[K, List[K]]: + if isinstance(index, int): + return self.__data[index] + elif isinstance(index, slice): + return self.__data[index] + else: + raise TypeError("Invalid argument type.") class ArrayMap(Mapping[K, V]): def __init__(self, data: list): From ab1fe7629dbabb938e18c31d572d903f6f9482f3 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 27 Feb 2024 19:31:23 -0800 Subject: [PATCH 28/73] We want things to be as private as possible. private is two underscores `__`, protected is one `_`. --- python/selfie-lib/selfie_lib/LineReader.py | 33 +++++++++++----------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/python/selfie-lib/selfie_lib/LineReader.py b/python/selfie-lib/selfie_lib/LineReader.py index 9c007236..60aad77a 100644 --- a/python/selfie-lib/selfie_lib/LineReader.py +++ b/python/selfie-lib/selfie_lib/LineReader.py @@ -1,36 +1,35 @@ import io + class LineReader: def __init__(self, content: bytes): - self.buffer = io.BytesIO(content) - # Making the method private as per the comment - self.uses_unix_newlines = self._detect_newline_type() - self.line_count = 0 # Initialize line count + self.__buffer = io.BytesIO(content) + self.__uses_unix_newlines = self.__detect_newline_type() + self.__line_count = 0 # Initialize line count @classmethod def for_binary(cls, content: bytes): return cls(content) - + @classmethod def for_string(cls, content: str): - return cls(content.encode('utf-8')) + return cls(content.encode("utf-8")) - # Now a private method - def _detect_newline_type(self) -> bool: - first_line = self.buffer.readline() - self.buffer.seek(0) # Reset buffer for actual reading - return b'\r\n' not in first_line + def __detect_newline_type(self) -> bool: + first_line = self.__buffer.readline() + self.__buffer.seek(0) # Reset buffer for actual reading + return b"\r\n" not in first_line def unix_newlines(self) -> bool: - return self.uses_unix_newlines + return self.__uses_unix_newlines def read_line(self) -> str: - line_bytes = self.buffer.readline() + line_bytes = self.__buffer.readline() if line_bytes: - self.line_count += 1 # Increment line count for each line read - line = line_bytes.decode('utf-8') - return line.rstrip('\r\n' if not self.uses_unix_newlines else '\n') + self.__line_count += 1 # Increment line count for each line read + line = line_bytes.decode("utf-8") + return line.rstrip("\r\n" if not self.__uses_unix_newlines else "\n") # Method to get the current line number def get_line_number(self) -> int: - return self.line_count + return self.__line_count From 4b727d548ec1ce235a6305d61c6538b03483d7e6 Mon Sep 17 00:00:00 2001 From: Harvir Sahota Date: Tue, 27 Feb 2024 20:16:56 -0800 Subject: [PATCH 29/73] Fix type mismatch --- python/selfie-lib/selfie_lib/ArrayMap.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/python/selfie-lib/selfie_lib/ArrayMap.py b/python/selfie-lib/selfie_lib/ArrayMap.py index 7d7ba570..5cb60673 100644 --- a/python/selfie-lib/selfie_lib/ArrayMap.py +++ b/python/selfie-lib/selfie_lib/ArrayMap.py @@ -1,4 +1,4 @@ -from collections.abc import Set, Sequence, Iterator, Mapping +from collections.abc import Set, Iterator, Mapping from typing import List, TypeVar, Union from abc import abstractmethod @@ -6,12 +6,12 @@ V = TypeVar('V') K = TypeVar('K') -class ListBackedSet(Set[T], Sequence[T]): +class ListBackedSet(Set[T]): @abstractmethod def __len__(self) -> int: ... @abstractmethod - def __getitem__(self, index: Union[int, slice]) -> Union[T, Sequence[T]]: ... + def __getitem__(self, index: Union[int, slice]) -> Union[T, List[T]]: ... def __contains__(self, item: object) -> bool: for i in range(len(self)): From a718872d2d5effc42280de72774144fd0956d705 Mon Sep 17 00:00:00 2001 From: Harvir Sahota Date: Wed, 28 Feb 2024 10:28:49 -0800 Subject: [PATCH 30/73] Fix changes --- python/selfie-lib/selfie_lib/ArrayMap.py | 49 ++++++++++++++++-------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/python/selfie-lib/selfie_lib/ArrayMap.py b/python/selfie-lib/selfie_lib/ArrayMap.py index 5cb60673..37734015 100644 --- a/python/selfie-lib/selfie_lib/ArrayMap.py +++ b/python/selfie-lib/selfie_lib/ArrayMap.py @@ -1,12 +1,12 @@ from collections.abc import Set, Iterator, Mapping from typing import List, TypeVar, Union -from abc import abstractmethod +from abc import abstractmethod, ABC T = TypeVar('T') V = TypeVar('V') K = TypeVar('K') -class ListBackedSet(Set[T]): +class ListBackedSet(Set[T], ABC): @abstractmethod def __len__(self) -> int: ... @@ -20,9 +20,17 @@ def __contains__(self, item: object) -> bool: return False class ArraySet(ListBackedSet[K]): + __empty_set = None + def __init__(self, data: List[K]): self.__data = data + @classmethod + def empty(cls) -> 'ArraySet[K]': + if cls.__empty_set is None: + cls.__empty_set = cls([]) + return cls.__empty_set + def __len__(self) -> int: return len(self.__data) @@ -33,32 +41,41 @@ def __getitem__(self, index: Union[int, slice]) -> Union[K, List[K]]: return self.__data[index] else: raise TypeError("Invalid argument type.") + + def plusOrThis(self, element: K) -> 'ArraySet[K]': + if element not in self.__data: + self.__data.append(element) + return self class ArrayMap(Mapping[K, V]): + __empty_map = None + def __init__(self, data: list): - self.data = data + self.__data = data @classmethod - def empty(cls): - return cls([]) + def empty(cls) -> 'ArrayMap[K, V]': + if cls.__empty_map is None: + cls.__empty_map = cls([]) + return cls.__empty_map def __getitem__(self, key: K) -> V: - index = self._binary_search_key(key) + index = self.__binary_search_key(key) if index >= 0: - return self.data[2 * index + 1] + return self.__data[2 * index + 1] raise KeyError(key) def __iter__(self) -> Iterator[K]: - return (self.data[i] for i in range(0, len(self.data), 2)) + return (self.__data[i] for i in range(0, len(self.__data), 2)) def __len__(self) -> int: - return len(self.data) // 2 + return len(self.__data) // 2 - def _binary_search_key(self, key: K) -> int: - low, high = 0, (len(self.data) // 2) - 1 + def __binary_search_key(self, key: K) -> int: + low, high = 0, (len(self.__data) // 2) - 1 while low <= high: mid = (low + high) // 2 - mid_key = self.data[2 * mid] + mid_key = self.__data[2 * mid] if mid_key < key: low = mid + 1 elif mid_key > key: @@ -68,11 +85,11 @@ def _binary_search_key(self, key: K) -> int: return -(low + 1) def plus(self, key: K, value: V) -> 'ArrayMap[K, V]': - index = self._binary_search_key(key) + index = self.__binary_search_key(key) if index >= 0: raise ValueError("Key already exists") insert_at = -(index + 1) - new_data = self.data[:] + new_data = self.__data[:] new_data[insert_at * 2:insert_at * 2] = [key, value] return ArrayMap(new_data) @@ -80,7 +97,7 @@ def minus_sorted_indices(self, indicesToRemove: List[int]) -> 'ArrayMap[K, V]': if not indicesToRemove: return self newData = [] - for i in range(0, len(self.data), 2): + for i in range(0, len(self.__data), 2): if i // 2 not in indicesToRemove: - newData.extend(self.data[i:i + 2]) + newData.extend(self.__data[i:i + 2]) return ArrayMap(newData) From 98f9485c0a2c4a4621b8e07c7db5be0376e9802b Mon Sep 17 00:00:00 2001 From: Harvir Sahota Date: Wed, 28 Feb 2024 11:05:52 -0800 Subject: [PATCH 31/73] Fix mutation issue and sorted issue --- python/selfie-lib/selfie_lib/ArrayMap.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/python/selfie-lib/selfie_lib/ArrayMap.py b/python/selfie-lib/selfie_lib/ArrayMap.py index 37734015..c8c2f5e9 100644 --- a/python/selfie-lib/selfie_lib/ArrayMap.py +++ b/python/selfie-lib/selfie_lib/ArrayMap.py @@ -23,7 +23,12 @@ class ArraySet(ListBackedSet[K]): __empty_set = None def __init__(self, data: List[K]): - self.__data = data + self.__data = [] + for item in data: + self.plusOrThis(item) + + def __iter__(self) -> Iterator[K]: + return iter(self.__data) @classmethod def empty(cls) -> 'ArraySet[K]': @@ -41,11 +46,18 @@ def __getitem__(self, index: Union[int, slice]) -> Union[K, List[K]]: return self.__data[index] else: raise TypeError("Invalid argument type.") - + def plusOrThis(self, element: K) -> 'ArraySet[K]': - if element not in self.__data: - self.__data.append(element) - return self + new_data = [] + added = False + for item in self.__data: + if not added and element < item: + new_data.append(element) + added = True + new_data.append(item) + if not added: + new_data.append(element) + return ArraySet(new_data) class ArrayMap(Mapping[K, V]): __empty_map = None From a77bebc0594d738f13cb3475e3fdb24a13f766e7 Mon Sep 17 00:00:00 2001 From: Harvir Sahota Date: Wed, 28 Feb 2024 15:56:36 -0800 Subject: [PATCH 32/73] Make constructor private --- python/selfie-lib/selfie_lib/ArrayMap.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/selfie-lib/selfie_lib/ArrayMap.py b/python/selfie-lib/selfie_lib/ArrayMap.py index c8c2f5e9..06f2d069 100644 --- a/python/selfie-lib/selfie_lib/ArrayMap.py +++ b/python/selfie-lib/selfie_lib/ArrayMap.py @@ -25,7 +25,7 @@ class ArraySet(ListBackedSet[K]): def __init__(self, data: List[K]): self.__data = [] for item in data: - self.plusOrThis(item) + self.__plusOrThis(item) def __iter__(self) -> Iterator[K]: return iter(self.__data) @@ -47,7 +47,7 @@ def __getitem__(self, index: Union[int, slice]) -> Union[K, List[K]]: else: raise TypeError("Invalid argument type.") - def plusOrThis(self, element: K) -> 'ArraySet[K]': + def __plusOrThis(self, element: K) -> 'ArraySet[K]': new_data = [] added = False for item in self.__data: From 994616742452c2d4b4b09818a4e1e8b3b5dfe4d5 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 29 Feb 2024 00:27:54 -0800 Subject: [PATCH 33/73] The `__empty` sentinels should be on the *class*, not each instance. --- python/selfie-lib/selfie_lib/ArrayMap.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/python/selfie-lib/selfie_lib/ArrayMap.py b/python/selfie-lib/selfie_lib/ArrayMap.py index 06f2d069..93f09603 100644 --- a/python/selfie-lib/selfie_lib/ArrayMap.py +++ b/python/selfie-lib/selfie_lib/ArrayMap.py @@ -20,8 +20,6 @@ def __contains__(self, item: object) -> bool: return False class ArraySet(ListBackedSet[K]): - __empty_set = None - def __init__(self, data: List[K]): self.__data = [] for item in data: @@ -32,9 +30,9 @@ def __iter__(self) -> Iterator[K]: @classmethod def empty(cls) -> 'ArraySet[K]': - if cls.__empty_set is None: - cls.__empty_set = cls([]) - return cls.__empty_set + if not hasattr(cls, '__EMPTY'): + cls.__EMPTY = cls([]) + return cls.__EMPTY def __len__(self) -> int: return len(self.__data) @@ -60,16 +58,14 @@ def __plusOrThis(self, element: K) -> 'ArraySet[K]': return ArraySet(new_data) class ArrayMap(Mapping[K, V]): - __empty_map = None - def __init__(self, data: list): self.__data = data @classmethod def empty(cls) -> 'ArrayMap[K, V]': - if cls.__empty_map is None: - cls.__empty_map = cls([]) - return cls.__empty_map + if not hasattr(cls, '__EMPTY'): + cls.__EMPTY = cls([]) + return cls.__EMPTY def __getitem__(self, key: K) -> V: index = self.__binary_search_key(key) From 7597c2c8fc0aa51377c0601cff9a1da71cca900a Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 29 Feb 2024 00:41:29 -0800 Subject: [PATCH 34/73] `plusOrThis` should be public. --- python/selfie-lib/selfie_lib/ArrayMap.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/selfie-lib/selfie_lib/ArrayMap.py b/python/selfie-lib/selfie_lib/ArrayMap.py index 93f09603..f58e892c 100644 --- a/python/selfie-lib/selfie_lib/ArrayMap.py +++ b/python/selfie-lib/selfie_lib/ArrayMap.py @@ -23,7 +23,7 @@ class ArraySet(ListBackedSet[K]): def __init__(self, data: List[K]): self.__data = [] for item in data: - self.__plusOrThis(item) + self.plusOrThis(item) def __iter__(self) -> Iterator[K]: return iter(self.__data) @@ -45,7 +45,7 @@ def __getitem__(self, index: Union[int, slice]) -> Union[K, List[K]]: else: raise TypeError("Invalid argument type.") - def __plusOrThis(self, element: K) -> 'ArraySet[K]': + def plusOrThis(self, element: K) -> 'ArraySet[K]': new_data = [] added = False for item in self.__data: From b3bdaf421478e01a0bbe91b4c9c9bd5300032be6 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 29 Feb 2024 00:46:27 -0800 Subject: [PATCH 35/73] Hide the `ArraySet` constructor. --- python/selfie-lib/selfie_lib/ArrayMap.py | 29 ++++++++++++++---------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/python/selfie-lib/selfie_lib/ArrayMap.py b/python/selfie-lib/selfie_lib/ArrayMap.py index f58e892c..3f01ab88 100644 --- a/python/selfie-lib/selfie_lib/ArrayMap.py +++ b/python/selfie-lib/selfie_lib/ArrayMap.py @@ -20,11 +20,18 @@ def __contains__(self, item: object) -> bool: return False class ArraySet(ListBackedSet[K]): + __data: List[K] + def __init__(self, data: List[K]): - self.__data = [] - for item in data: - self.plusOrThis(item) + raise NotImplementedError("Use ArraySet.empty() instead") + @classmethod + def __create(cls, data: List[K]) -> 'ArraySet[K]': + # Create a new instance without calling __init__ + instance = super().__new__(cls) + instance.__data = data + return instance + def __iter__(self) -> Iterator[K]: return iter(self.__data) @@ -46,16 +53,14 @@ def __getitem__(self, index: Union[int, slice]) -> Union[K, List[K]]: raise TypeError("Invalid argument type.") def plusOrThis(self, element: K) -> 'ArraySet[K]': - new_data = [] - added = False - for item in self.__data: - if not added and element < item: - new_data.append(element) - added = True - new_data.append(item) - if not added: + if element in self.__data: + return self + else: + new_data = self.__data[:] new_data.append(element) - return ArraySet(new_data) + new_data.sort() # type: ignore[reportOperatorIssue] + return ArraySet.__create(new_data) + class ArrayMap(Mapping[K, V]): def __init__(self, data: list): From 7b51b1390f68c34be3d97bff1dcd1253ed407f46 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 29 Feb 2024 00:47:14 -0800 Subject: [PATCH 36/73] Add a note to hide the ArrayMap constructor in the same way. --- python/selfie-lib/selfie_lib/ArrayMap.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python/selfie-lib/selfie_lib/ArrayMap.py b/python/selfie-lib/selfie_lib/ArrayMap.py index 3f01ab88..78b4cc82 100644 --- a/python/selfie-lib/selfie_lib/ArrayMap.py +++ b/python/selfie-lib/selfie_lib/ArrayMap.py @@ -64,6 +64,7 @@ def plusOrThis(self, element: K) -> 'ArraySet[K]': class ArrayMap(Mapping[K, V]): def __init__(self, data: list): + # TODO: hide this constructor self.__data = data @classmethod From 4795d287b03f666df554350f9feafb40dbc5198e Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 29 Feb 2024 00:49:07 -0800 Subject: [PATCH 37/73] Add a note to use special sort order for strings someday. --- python/selfie-lib/selfie_lib/ArrayMap.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/python/selfie-lib/selfie_lib/ArrayMap.py b/python/selfie-lib/selfie_lib/ArrayMap.py index 78b4cc82..b8281796 100644 --- a/python/selfie-lib/selfie_lib/ArrayMap.py +++ b/python/selfie-lib/selfie_lib/ArrayMap.py @@ -53,6 +53,7 @@ def __getitem__(self, index: Union[int, slice]) -> Union[K, List[K]]: raise TypeError("Invalid argument type.") def plusOrThis(self, element: K) -> 'ArraySet[K]': + # TODO: use binary search, and also special sort order for strings if element in self.__data: return self else: @@ -64,7 +65,7 @@ def plusOrThis(self, element: K) -> 'ArraySet[K]': class ArrayMap(Mapping[K, V]): def __init__(self, data: list): - # TODO: hide this constructor + # TODO: hide this constructor as done in ArraySet self.__data = data @classmethod @@ -86,6 +87,7 @@ def __len__(self) -> int: return len(self.__data) // 2 def __binary_search_key(self, key: K) -> int: + # TODO: special sort order for strings low, high = 0, (len(self.__data) // 2) - 1 while low <= high: mid = (low + high) // 2 From ca47594b5a09d48c0ce26caa7cbe5ade2048ebd9 Mon Sep 17 00:00:00 2001 From: Selina Delgado Date: Thu, 29 Feb 2024 10:43:26 -0800 Subject: [PATCH 38/73] first commit --- python/selfie-lib/selfie_lib/SourceFile.py | 0 python/selfie-lib/tests/SourceFile_test.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 python/selfie-lib/selfie_lib/SourceFile.py create mode 100644 python/selfie-lib/tests/SourceFile_test.py diff --git a/python/selfie-lib/selfie_lib/SourceFile.py b/python/selfie-lib/selfie_lib/SourceFile.py new file mode 100644 index 00000000..e69de29b diff --git a/python/selfie-lib/tests/SourceFile_test.py b/python/selfie-lib/tests/SourceFile_test.py new file mode 100644 index 00000000..e69de29b From 255e5fd29b049e0a7bee955f2512846bd766ec9c Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 29 Feb 2024 11:47:14 -0800 Subject: [PATCH 39/73] Use ruff format correctly. --- .github/workflows/python-ci.yml | 8 ++-- python/selfie-lib/selfie_lib/ArrayMap.py | 42 ++++++++++--------- python/selfie-lib/selfie_lib/Slice.py | 49 +++++++++++++--------- python/selfie-lib/selfie_lib/__init__.py | 1 - python/selfie-lib/tests/ArrayMap_test.py | 32 +++++++++++--- python/selfie-lib/tests/LineReader_test.py | 7 ++++ python/selfie-lib/tests/Slice_test.py | 5 ++- 7 files changed, 94 insertions(+), 50 deletions(-) diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 2e0453b1..6f7ecdeb 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -3,7 +3,7 @@ on: branches: [main] pull_request: paths: - - 'python/**' + - "python/**" defaults: run: working-directory: python/selfie-lib @@ -24,9 +24,9 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version-file: 'python/selfie-lib/pyproject.toml' - cache: 'poetry' + python-version-file: "python/selfie-lib/pyproject.toml" + cache: "poetry" - run: poetry install - run: poetry run pytest -vv - run: poetry run pyright - - run: poetry run ruff check + - run: poetry run ruff format --check diff --git a/python/selfie-lib/selfie_lib/ArrayMap.py b/python/selfie-lib/selfie_lib/ArrayMap.py index b8281796..485fb296 100644 --- a/python/selfie-lib/selfie_lib/ArrayMap.py +++ b/python/selfie-lib/selfie_lib/ArrayMap.py @@ -2,16 +2,19 @@ from typing import List, TypeVar, Union from abc import abstractmethod, ABC -T = TypeVar('T') -V = TypeVar('V') -K = TypeVar('K') +T = TypeVar("T") +V = TypeVar("V") +K = TypeVar("K") + class ListBackedSet(Set[T], ABC): @abstractmethod - def __len__(self) -> int: ... + def __len__(self) -> int: + ... @abstractmethod - def __getitem__(self, index: Union[int, slice]) -> Union[T, List[T]]: ... + def __getitem__(self, index: Union[int, slice]) -> Union[T, List[T]]: + ... def __contains__(self, item: object) -> bool: for i in range(len(self)): @@ -19,6 +22,7 @@ def __contains__(self, item: object) -> bool: return True return False + class ArraySet(ListBackedSet[K]): __data: List[K] @@ -26,18 +30,18 @@ def __init__(self, data: List[K]): raise NotImplementedError("Use ArraySet.empty() instead") @classmethod - def __create(cls, data: List[K]) -> 'ArraySet[K]': + def __create(cls, data: List[K]) -> "ArraySet[K]": # Create a new instance without calling __init__ instance = super().__new__(cls) instance.__data = data return instance - + def __iter__(self) -> Iterator[K]: return iter(self.__data) @classmethod - def empty(cls) -> 'ArraySet[K]': - if not hasattr(cls, '__EMPTY'): + def empty(cls) -> "ArraySet[K]": + if not hasattr(cls, "__EMPTY"): cls.__EMPTY = cls([]) return cls.__EMPTY @@ -52,14 +56,14 @@ def __getitem__(self, index: Union[int, slice]) -> Union[K, List[K]]: else: raise TypeError("Invalid argument type.") - def plusOrThis(self, element: K) -> 'ArraySet[K]': + def plusOrThis(self, element: K) -> "ArraySet[K]": # TODO: use binary search, and also special sort order for strings if element in self.__data: return self else: new_data = self.__data[:] new_data.append(element) - new_data.sort() # type: ignore[reportOperatorIssue] + new_data.sort() # type: ignore[reportOperatorIssue] return ArraySet.__create(new_data) @@ -69,8 +73,8 @@ def __init__(self, data: list): self.__data = data @classmethod - def empty(cls) -> 'ArrayMap[K, V]': - if not hasattr(cls, '__EMPTY'): + def empty(cls) -> "ArrayMap[K, V]": + if not hasattr(cls, "__EMPTY"): cls.__EMPTY = cls([]) return cls.__EMPTY @@ -86,7 +90,7 @@ def __iter__(self) -> Iterator[K]: def __len__(self) -> int: return len(self.__data) // 2 - def __binary_search_key(self, key: K) -> int: + def __binary_search_key(self, key: K) -> int: # TODO: special sort order for strings low, high = 0, (len(self.__data) // 2) - 1 while low <= high: @@ -100,20 +104,20 @@ def __binary_search_key(self, key: K) -> int: return mid return -(low + 1) - def plus(self, key: K, value: V) -> 'ArrayMap[K, V]': - index = self.__binary_search_key(key) + def plus(self, key: K, value: V) -> "ArrayMap[K, V]": + index = self.__binary_search_key(key) if index >= 0: raise ValueError("Key already exists") insert_at = -(index + 1) new_data = self.__data[:] - new_data[insert_at * 2:insert_at * 2] = [key, value] + new_data[insert_at * 2 : insert_at * 2] = [key, value] return ArrayMap(new_data) - def minus_sorted_indices(self, indicesToRemove: List[int]) -> 'ArrayMap[K, V]': + def minus_sorted_indices(self, indicesToRemove: List[int]) -> "ArrayMap[K, V]": if not indicesToRemove: return self newData = [] for i in range(0, len(self.__data), 2): if i // 2 not in indicesToRemove: - newData.extend(self.__data[i:i + 2]) + newData.extend(self.__data[i : i + 2]) return ArrayMap(newData) diff --git a/python/selfie-lib/selfie_lib/Slice.py b/python/selfie-lib/selfie_lib/Slice.py index 63b15cdd..517b45be 100644 --- a/python/selfie-lib/selfie_lib/Slice.py +++ b/python/selfie-lib/selfie_lib/Slice.py @@ -1,18 +1,23 @@ -from typing import Optional -from typing import Union +from typing import Optional +from typing import Union from collections import Counter + class Slice: """Represents a slice of a base string from startIndex to endIndex.""" - def __init__(self, base: str, startIndex: int = 0, endIndex: Optional[int] = None) -> None: + def __init__( + self, base: str, startIndex: int = 0, endIndex: Optional[int] = None + ) -> None: self.base = base self.base = base self.startIndex = startIndex self.endIndex = endIndex if endIndex is not None else len(base) - assert 0 <= self.startIndex <= self.endIndex <= len(base), "Invalid start or end index" - + assert ( + 0 <= self.startIndex <= self.endIndex <= len(base) + ), "Invalid start or end index" + def __len__(self) -> int: return self.endIndex - self.startIndex @@ -21,10 +26,10 @@ def __getitem__(self, index: int) -> str: raise IndexError("Index out of range") return self.base[self.startIndex + index] - def subSequence(self, start: int, end: int) -> 'Slice': + def subSequence(self, start: int, end: int) -> "Slice": return Slice(self.base, self.startIndex + start, self.startIndex + end) - def trim(self) -> 'Slice': + def trim(self) -> "Slice": start, end = 0, len(self) while start < end and self[start].isspace(): start += 1 @@ -33,9 +38,9 @@ def trim(self) -> 'Slice': return self.subSequence(start, end) if start > 0 or end < len(self) else self def __str__(self) -> str: - return self.base[self.startIndex:self.endIndex] + return self.base[self.startIndex : self.endIndex] - def sameAs(self, other: Union['Slice', str]) -> bool: + def sameAs(self, other: Union["Slice", str]) -> bool: if isinstance(other, Slice): return str(self) == str(other) elif isinstance(other, str): @@ -48,18 +53,24 @@ def sameAs(self, other: Union['Slice', str]) -> bool: return False def indexOf(self, lookingFor: str, startOffset: int = 0) -> int: - result = self.base.find(lookingFor, self.startIndex + startOffset, self.endIndex) + result = self.base.find( + lookingFor, self.startIndex + startOffset, self.endIndex + ) return -1 if result == -1 else result - self.startIndex - def unixLine(self, count: int) -> 'Slice': + def unixLine(self, count: int) -> "Slice": assert count > 0, "Count must be positive" lineStart = 0 for i in range(1, count): - lineStart = self.indexOf('\n', lineStart) + lineStart = self.indexOf("\n", lineStart) assert lineStart >= 0, f"This string has only {i - 1} lines, not {count}" lineStart += 1 - lineEnd = self.indexOf('\n', lineStart) - return Slice(self.base, self.startIndex + lineStart, self.endIndex if lineEnd == -1 else self.startIndex + lineEnd) + lineEnd = self.indexOf("\n", lineStart) + return Slice( + self.base, + self.startIndex + lineStart, + self.endIndex if lineEnd == -1 else self.startIndex + lineEnd, + ) def __eq__(self, other: object) -> bool: if self is other: @@ -75,10 +86,10 @@ def __hash__(self) -> int: return h def replaceSelfWith(self, s: str) -> str: - return self.base[:self.startIndex] + s + self.base[self.endIndex:] - + return self.base[: self.startIndex] + s + self.base[self.endIndex :] + def count(self, char: str) -> int: - return Counter(self.base[self.startIndex:self.endIndex])[char] - + return Counter(self.base[self.startIndex : self.endIndex])[char] + def baseLineAtOffset(self, index: int) -> int: - return 1 + Slice(self.base, 0, index).count('\n') + return 1 + Slice(self.base, 0, index).count("\n") diff --git a/python/selfie-lib/selfie_lib/__init__.py b/python/selfie-lib/selfie_lib/__init__.py index e2859a5d..ad6c6317 100644 --- a/python/selfie-lib/selfie_lib/__init__.py +++ b/python/selfie-lib/selfie_lib/__init__.py @@ -1,3 +1,2 @@ from .LineReader import LineReader as LineReader from .Slice import Slice as Slice - diff --git a/python/selfie-lib/tests/ArrayMap_test.py b/python/selfie-lib/tests/ArrayMap_test.py index f3f078e8..0af17e5d 100644 --- a/python/selfie-lib/tests/ArrayMap_test.py +++ b/python/selfie-lib/tests/ArrayMap_test.py @@ -1,6 +1,7 @@ import pytest from selfie_lib.ArrayMap import ArrayMap + def assertEmpty(map): assert len(map) == 0 assert list(map.keys()) == [] @@ -11,6 +12,7 @@ def assertEmpty(map): assert map == {} assert map == ArrayMap.empty() + def assertSingle(map, key, value): assert len(map) == 1 assert set(map.keys()) == {key} @@ -22,6 +24,7 @@ def assertSingle(map, key, value): assert map == {key: value} assert map == ArrayMap.empty().plus(key, value) + def assertDouble(map, key1, value1, key2, value2): assert len(map) == 2 assert set(map.keys()) == {key1, key2} @@ -36,6 +39,7 @@ def assertDouble(map, key1, value1, key2, value2): assert map == ArrayMap.empty().plus(key1, value1).plus(key2, value2) assert map == ArrayMap.empty().plus(key2, value2).plus(key1, value1) + def assertTriple(map, key1, value1, key2, value2, key3, value3): assert len(map) == 3 assert set(map.keys()) == {key1, key2, key3} @@ -47,17 +51,22 @@ def assertTriple(map, key1, value1, key2, value2, key3, value3): with pytest.raises(KeyError): _ = map[key1 + "blah"] assert map == {key1: value1, key2: value2, key3: value3} - assert map == ArrayMap.empty().plus(key1, value1).plus(key2, value2).plus(key3, value3) + assert map == ArrayMap.empty().plus(key1, value1).plus(key2, value2).plus( + key3, value3 + ) + def test_empty(): assertEmpty(ArrayMap.empty()) + def test_single(): empty = ArrayMap.empty() single = empty.plus("one", "1") assertEmpty(empty) assertSingle(single, "one", "1") + def test_double(): empty = ArrayMap.empty() single = empty.plus("one", "1") @@ -71,10 +80,12 @@ def test_double(): single.plus("one", "2") assert str(context.value) == "Key already exists" + def test_triple(): triple = ArrayMap.empty().plus("1", "one").plus("2", "two").plus("3", "three") assertTriple(triple, "1", "one", "2", "two", "3", "three") + def test_multi(): test_triple() # Calling another test function directly is unusual but works triple = ArrayMap.empty().plus("2", "two").plus("3", "three").plus("1", "one") @@ -82,8 +93,15 @@ def test_multi(): triple = ArrayMap.empty().plus("3", "three").plus("1", "one").plus("2", "two") assertTriple(triple, "1", "one", "2", "two", "3", "three") + def test_minus_sorted_indices(): - initial_map = ArrayMap.empty().plus("1", "one").plus("2", "two").plus("3", "three").plus("4", "four") + initial_map = ( + ArrayMap.empty() + .plus("1", "one") + .plus("2", "two") + .plus("3", "three") + .plus("4", "four") + ) modified_map = initial_map.minus_sorted_indices([1, 3]) assert len(modified_map) == 2 assert list(modified_map.keys()) == ["1", "3"] @@ -94,6 +112,7 @@ def test_minus_sorted_indices(): _ = modified_map["4"] assert modified_map == {"1": "one", "3": "three"} + def test_plus_with_existing_keys(): map_with_duplicates = ArrayMap.empty().plus("a", "alpha").plus("b", "beta") with pytest.raises(ValueError): @@ -103,11 +122,14 @@ def test_plus_with_existing_keys(): assert updated_map["a"] == "alpha" assert updated_map["b"] == "beta" assert updated_map["c"] == "gamma" - modified_map = map_with_duplicates.minus_sorted_indices([0]).plus("a", "updated alpha") + modified_map = map_with_duplicates.minus_sorted_indices([0]).plus( + "a", "updated alpha" + ) assert len(modified_map) == 2 assert modified_map["a"] == "updated alpha" assert modified_map["b"] == "beta" + def test_map_length(): map = ArrayMap.empty() assert len(map) == 0, "Length should be 0 for an empty map" @@ -119,7 +141,7 @@ def test_map_length(): assert len(map) == 3, "Length should be 3 after adding a third item" map = map.minus_sorted_indices([1]) assert len(map) == 2, "Length should be 2 after removing one item" - map = map.minus_sorted_indices([0]) + map = map.minus_sorted_indices([0]) assert len(map) == 1, "Length should be 1 after removing another item" - map = map.minus_sorted_indices([0]) + map = map.minus_sorted_indices([0]) assert len(map) == 0, "Length should be 0 after removing all items" diff --git a/python/selfie-lib/tests/LineReader_test.py b/python/selfie-lib/tests/LineReader_test.py index f42b9308..95c40453 100644 --- a/python/selfie-lib/tests/LineReader_test.py +++ b/python/selfie-lib/tests/LineReader_test.py @@ -1,30 +1,36 @@ from selfie_lib import LineReader + def test_should_find_unix_separator_from_binary(): reader = LineReader.for_binary(b"This is a new line\n") assert reader.unix_newlines() is True assert reader.read_line() == "This is a new line" + def test_should_find_windows_separator_from_binary(): reader = LineReader.for_binary(b"This is a new line\r\n") assert reader.unix_newlines() is False assert reader.read_line() == "This is a new line" + def test_should_find_unix_separator_from_string(): reader = LineReader.for_string("This is a new line\n") assert reader.unix_newlines() is True assert reader.read_line() == "This is a new line" + def test_should_find_windows_separator_from_string(): reader = LineReader.for_string("This is a new line\r\n") assert reader.unix_newlines() is False assert reader.read_line() == "This is a new line" + def test_should_get_unix_line_separator_when_there_is_none(): reader = LineReader.for_binary(b"This is a new line") assert reader.unix_newlines() is True assert reader.read_line() == "This is a new line" + def test_should_read_next_line_without_problem(): reader = LineReader.for_binary(b"First\r\nSecond\r\n") assert reader.unix_newlines() is False @@ -33,6 +39,7 @@ def test_should_read_next_line_without_problem(): assert reader.read_line() == "Second" assert reader.unix_newlines() is False + def test_should_use_first_line_separator_and_ignore_next(): reader = LineReader.for_binary(b"First\r\nAnother separator\n") assert reader.unix_newlines() is False diff --git a/python/selfie-lib/tests/Slice_test.py b/python/selfie-lib/tests/Slice_test.py index fad1af52..42968e7c 100644 --- a/python/selfie-lib/tests/Slice_test.py +++ b/python/selfie-lib/tests/Slice_test.py @@ -1,13 +1,14 @@ from selfie_lib import Slice + def test_unixLine(): slice_1 = Slice("A single line") assert str(slice_1.unixLine(1)) == "A single line" - + one_two_three = Slice("\nI am the first\nI, the second\n\nFOURTH\n") assert str(one_two_three.unixLine(1)) == "" assert str(one_two_three.unixLine(2)) == "I am the first" assert str(one_two_three.unixLine(3)) == "I, the second" assert str(one_two_three.unixLine(4)) == "" assert str(one_two_three.unixLine(5)) == "FOURTH" - assert str(one_two_three.unixLine(6)) == "" \ No newline at end of file + assert str(one_two_three.unixLine(6)) == "" From e4d347c1aef9e427beb22ed335658236d850c099 Mon Sep 17 00:00:00 2001 From: Harvir Sahota Date: Thu, 29 Feb 2024 21:47:27 -0800 Subject: [PATCH 40/73] Fix format --- python/selfie-lib/selfie_lib/ArrayMap.py | 42 ++++++++++--------- python/selfie-lib/selfie_lib/Slice.py | 49 +++++++++++++--------- python/selfie-lib/selfie_lib/__init__.py | 1 - python/selfie-lib/tests/ArrayMap_test.py | 32 +++++++++++--- python/selfie-lib/tests/LineReader_test.py | 7 ++++ python/selfie-lib/tests/Slice_test.py | 5 ++- 6 files changed, 90 insertions(+), 46 deletions(-) diff --git a/python/selfie-lib/selfie_lib/ArrayMap.py b/python/selfie-lib/selfie_lib/ArrayMap.py index b8281796..485fb296 100644 --- a/python/selfie-lib/selfie_lib/ArrayMap.py +++ b/python/selfie-lib/selfie_lib/ArrayMap.py @@ -2,16 +2,19 @@ from typing import List, TypeVar, Union from abc import abstractmethod, ABC -T = TypeVar('T') -V = TypeVar('V') -K = TypeVar('K') +T = TypeVar("T") +V = TypeVar("V") +K = TypeVar("K") + class ListBackedSet(Set[T], ABC): @abstractmethod - def __len__(self) -> int: ... + def __len__(self) -> int: + ... @abstractmethod - def __getitem__(self, index: Union[int, slice]) -> Union[T, List[T]]: ... + def __getitem__(self, index: Union[int, slice]) -> Union[T, List[T]]: + ... def __contains__(self, item: object) -> bool: for i in range(len(self)): @@ -19,6 +22,7 @@ def __contains__(self, item: object) -> bool: return True return False + class ArraySet(ListBackedSet[K]): __data: List[K] @@ -26,18 +30,18 @@ def __init__(self, data: List[K]): raise NotImplementedError("Use ArraySet.empty() instead") @classmethod - def __create(cls, data: List[K]) -> 'ArraySet[K]': + def __create(cls, data: List[K]) -> "ArraySet[K]": # Create a new instance without calling __init__ instance = super().__new__(cls) instance.__data = data return instance - + def __iter__(self) -> Iterator[K]: return iter(self.__data) @classmethod - def empty(cls) -> 'ArraySet[K]': - if not hasattr(cls, '__EMPTY'): + def empty(cls) -> "ArraySet[K]": + if not hasattr(cls, "__EMPTY"): cls.__EMPTY = cls([]) return cls.__EMPTY @@ -52,14 +56,14 @@ def __getitem__(self, index: Union[int, slice]) -> Union[K, List[K]]: else: raise TypeError("Invalid argument type.") - def plusOrThis(self, element: K) -> 'ArraySet[K]': + def plusOrThis(self, element: K) -> "ArraySet[K]": # TODO: use binary search, and also special sort order for strings if element in self.__data: return self else: new_data = self.__data[:] new_data.append(element) - new_data.sort() # type: ignore[reportOperatorIssue] + new_data.sort() # type: ignore[reportOperatorIssue] return ArraySet.__create(new_data) @@ -69,8 +73,8 @@ def __init__(self, data: list): self.__data = data @classmethod - def empty(cls) -> 'ArrayMap[K, V]': - if not hasattr(cls, '__EMPTY'): + def empty(cls) -> "ArrayMap[K, V]": + if not hasattr(cls, "__EMPTY"): cls.__EMPTY = cls([]) return cls.__EMPTY @@ -86,7 +90,7 @@ def __iter__(self) -> Iterator[K]: def __len__(self) -> int: return len(self.__data) // 2 - def __binary_search_key(self, key: K) -> int: + def __binary_search_key(self, key: K) -> int: # TODO: special sort order for strings low, high = 0, (len(self.__data) // 2) - 1 while low <= high: @@ -100,20 +104,20 @@ def __binary_search_key(self, key: K) -> int: return mid return -(low + 1) - def plus(self, key: K, value: V) -> 'ArrayMap[K, V]': - index = self.__binary_search_key(key) + def plus(self, key: K, value: V) -> "ArrayMap[K, V]": + index = self.__binary_search_key(key) if index >= 0: raise ValueError("Key already exists") insert_at = -(index + 1) new_data = self.__data[:] - new_data[insert_at * 2:insert_at * 2] = [key, value] + new_data[insert_at * 2 : insert_at * 2] = [key, value] return ArrayMap(new_data) - def minus_sorted_indices(self, indicesToRemove: List[int]) -> 'ArrayMap[K, V]': + def minus_sorted_indices(self, indicesToRemove: List[int]) -> "ArrayMap[K, V]": if not indicesToRemove: return self newData = [] for i in range(0, len(self.__data), 2): if i // 2 not in indicesToRemove: - newData.extend(self.__data[i:i + 2]) + newData.extend(self.__data[i : i + 2]) return ArrayMap(newData) diff --git a/python/selfie-lib/selfie_lib/Slice.py b/python/selfie-lib/selfie_lib/Slice.py index 63b15cdd..517b45be 100644 --- a/python/selfie-lib/selfie_lib/Slice.py +++ b/python/selfie-lib/selfie_lib/Slice.py @@ -1,18 +1,23 @@ -from typing import Optional -from typing import Union +from typing import Optional +from typing import Union from collections import Counter + class Slice: """Represents a slice of a base string from startIndex to endIndex.""" - def __init__(self, base: str, startIndex: int = 0, endIndex: Optional[int] = None) -> None: + def __init__( + self, base: str, startIndex: int = 0, endIndex: Optional[int] = None + ) -> None: self.base = base self.base = base self.startIndex = startIndex self.endIndex = endIndex if endIndex is not None else len(base) - assert 0 <= self.startIndex <= self.endIndex <= len(base), "Invalid start or end index" - + assert ( + 0 <= self.startIndex <= self.endIndex <= len(base) + ), "Invalid start or end index" + def __len__(self) -> int: return self.endIndex - self.startIndex @@ -21,10 +26,10 @@ def __getitem__(self, index: int) -> str: raise IndexError("Index out of range") return self.base[self.startIndex + index] - def subSequence(self, start: int, end: int) -> 'Slice': + def subSequence(self, start: int, end: int) -> "Slice": return Slice(self.base, self.startIndex + start, self.startIndex + end) - def trim(self) -> 'Slice': + def trim(self) -> "Slice": start, end = 0, len(self) while start < end and self[start].isspace(): start += 1 @@ -33,9 +38,9 @@ def trim(self) -> 'Slice': return self.subSequence(start, end) if start > 0 or end < len(self) else self def __str__(self) -> str: - return self.base[self.startIndex:self.endIndex] + return self.base[self.startIndex : self.endIndex] - def sameAs(self, other: Union['Slice', str]) -> bool: + def sameAs(self, other: Union["Slice", str]) -> bool: if isinstance(other, Slice): return str(self) == str(other) elif isinstance(other, str): @@ -48,18 +53,24 @@ def sameAs(self, other: Union['Slice', str]) -> bool: return False def indexOf(self, lookingFor: str, startOffset: int = 0) -> int: - result = self.base.find(lookingFor, self.startIndex + startOffset, self.endIndex) + result = self.base.find( + lookingFor, self.startIndex + startOffset, self.endIndex + ) return -1 if result == -1 else result - self.startIndex - def unixLine(self, count: int) -> 'Slice': + def unixLine(self, count: int) -> "Slice": assert count > 0, "Count must be positive" lineStart = 0 for i in range(1, count): - lineStart = self.indexOf('\n', lineStart) + lineStart = self.indexOf("\n", lineStart) assert lineStart >= 0, f"This string has only {i - 1} lines, not {count}" lineStart += 1 - lineEnd = self.indexOf('\n', lineStart) - return Slice(self.base, self.startIndex + lineStart, self.endIndex if lineEnd == -1 else self.startIndex + lineEnd) + lineEnd = self.indexOf("\n", lineStart) + return Slice( + self.base, + self.startIndex + lineStart, + self.endIndex if lineEnd == -1 else self.startIndex + lineEnd, + ) def __eq__(self, other: object) -> bool: if self is other: @@ -75,10 +86,10 @@ def __hash__(self) -> int: return h def replaceSelfWith(self, s: str) -> str: - return self.base[:self.startIndex] + s + self.base[self.endIndex:] - + return self.base[: self.startIndex] + s + self.base[self.endIndex :] + def count(self, char: str) -> int: - return Counter(self.base[self.startIndex:self.endIndex])[char] - + return Counter(self.base[self.startIndex : self.endIndex])[char] + def baseLineAtOffset(self, index: int) -> int: - return 1 + Slice(self.base, 0, index).count('\n') + return 1 + Slice(self.base, 0, index).count("\n") diff --git a/python/selfie-lib/selfie_lib/__init__.py b/python/selfie-lib/selfie_lib/__init__.py index e2859a5d..ad6c6317 100644 --- a/python/selfie-lib/selfie_lib/__init__.py +++ b/python/selfie-lib/selfie_lib/__init__.py @@ -1,3 +1,2 @@ from .LineReader import LineReader as LineReader from .Slice import Slice as Slice - diff --git a/python/selfie-lib/tests/ArrayMap_test.py b/python/selfie-lib/tests/ArrayMap_test.py index f3f078e8..0af17e5d 100644 --- a/python/selfie-lib/tests/ArrayMap_test.py +++ b/python/selfie-lib/tests/ArrayMap_test.py @@ -1,6 +1,7 @@ import pytest from selfie_lib.ArrayMap import ArrayMap + def assertEmpty(map): assert len(map) == 0 assert list(map.keys()) == [] @@ -11,6 +12,7 @@ def assertEmpty(map): assert map == {} assert map == ArrayMap.empty() + def assertSingle(map, key, value): assert len(map) == 1 assert set(map.keys()) == {key} @@ -22,6 +24,7 @@ def assertSingle(map, key, value): assert map == {key: value} assert map == ArrayMap.empty().plus(key, value) + def assertDouble(map, key1, value1, key2, value2): assert len(map) == 2 assert set(map.keys()) == {key1, key2} @@ -36,6 +39,7 @@ def assertDouble(map, key1, value1, key2, value2): assert map == ArrayMap.empty().plus(key1, value1).plus(key2, value2) assert map == ArrayMap.empty().plus(key2, value2).plus(key1, value1) + def assertTriple(map, key1, value1, key2, value2, key3, value3): assert len(map) == 3 assert set(map.keys()) == {key1, key2, key3} @@ -47,17 +51,22 @@ def assertTriple(map, key1, value1, key2, value2, key3, value3): with pytest.raises(KeyError): _ = map[key1 + "blah"] assert map == {key1: value1, key2: value2, key3: value3} - assert map == ArrayMap.empty().plus(key1, value1).plus(key2, value2).plus(key3, value3) + assert map == ArrayMap.empty().plus(key1, value1).plus(key2, value2).plus( + key3, value3 + ) + def test_empty(): assertEmpty(ArrayMap.empty()) + def test_single(): empty = ArrayMap.empty() single = empty.plus("one", "1") assertEmpty(empty) assertSingle(single, "one", "1") + def test_double(): empty = ArrayMap.empty() single = empty.plus("one", "1") @@ -71,10 +80,12 @@ def test_double(): single.plus("one", "2") assert str(context.value) == "Key already exists" + def test_triple(): triple = ArrayMap.empty().plus("1", "one").plus("2", "two").plus("3", "three") assertTriple(triple, "1", "one", "2", "two", "3", "three") + def test_multi(): test_triple() # Calling another test function directly is unusual but works triple = ArrayMap.empty().plus("2", "two").plus("3", "three").plus("1", "one") @@ -82,8 +93,15 @@ def test_multi(): triple = ArrayMap.empty().plus("3", "three").plus("1", "one").plus("2", "two") assertTriple(triple, "1", "one", "2", "two", "3", "three") + def test_minus_sorted_indices(): - initial_map = ArrayMap.empty().plus("1", "one").plus("2", "two").plus("3", "three").plus("4", "four") + initial_map = ( + ArrayMap.empty() + .plus("1", "one") + .plus("2", "two") + .plus("3", "three") + .plus("4", "four") + ) modified_map = initial_map.minus_sorted_indices([1, 3]) assert len(modified_map) == 2 assert list(modified_map.keys()) == ["1", "3"] @@ -94,6 +112,7 @@ def test_minus_sorted_indices(): _ = modified_map["4"] assert modified_map == {"1": "one", "3": "three"} + def test_plus_with_existing_keys(): map_with_duplicates = ArrayMap.empty().plus("a", "alpha").plus("b", "beta") with pytest.raises(ValueError): @@ -103,11 +122,14 @@ def test_plus_with_existing_keys(): assert updated_map["a"] == "alpha" assert updated_map["b"] == "beta" assert updated_map["c"] == "gamma" - modified_map = map_with_duplicates.minus_sorted_indices([0]).plus("a", "updated alpha") + modified_map = map_with_duplicates.minus_sorted_indices([0]).plus( + "a", "updated alpha" + ) assert len(modified_map) == 2 assert modified_map["a"] == "updated alpha" assert modified_map["b"] == "beta" + def test_map_length(): map = ArrayMap.empty() assert len(map) == 0, "Length should be 0 for an empty map" @@ -119,7 +141,7 @@ def test_map_length(): assert len(map) == 3, "Length should be 3 after adding a third item" map = map.minus_sorted_indices([1]) assert len(map) == 2, "Length should be 2 after removing one item" - map = map.minus_sorted_indices([0]) + map = map.minus_sorted_indices([0]) assert len(map) == 1, "Length should be 1 after removing another item" - map = map.minus_sorted_indices([0]) + map = map.minus_sorted_indices([0]) assert len(map) == 0, "Length should be 0 after removing all items" diff --git a/python/selfie-lib/tests/LineReader_test.py b/python/selfie-lib/tests/LineReader_test.py index f42b9308..95c40453 100644 --- a/python/selfie-lib/tests/LineReader_test.py +++ b/python/selfie-lib/tests/LineReader_test.py @@ -1,30 +1,36 @@ from selfie_lib import LineReader + def test_should_find_unix_separator_from_binary(): reader = LineReader.for_binary(b"This is a new line\n") assert reader.unix_newlines() is True assert reader.read_line() == "This is a new line" + def test_should_find_windows_separator_from_binary(): reader = LineReader.for_binary(b"This is a new line\r\n") assert reader.unix_newlines() is False assert reader.read_line() == "This is a new line" + def test_should_find_unix_separator_from_string(): reader = LineReader.for_string("This is a new line\n") assert reader.unix_newlines() is True assert reader.read_line() == "This is a new line" + def test_should_find_windows_separator_from_string(): reader = LineReader.for_string("This is a new line\r\n") assert reader.unix_newlines() is False assert reader.read_line() == "This is a new line" + def test_should_get_unix_line_separator_when_there_is_none(): reader = LineReader.for_binary(b"This is a new line") assert reader.unix_newlines() is True assert reader.read_line() == "This is a new line" + def test_should_read_next_line_without_problem(): reader = LineReader.for_binary(b"First\r\nSecond\r\n") assert reader.unix_newlines() is False @@ -33,6 +39,7 @@ def test_should_read_next_line_without_problem(): assert reader.read_line() == "Second" assert reader.unix_newlines() is False + def test_should_use_first_line_separator_and_ignore_next(): reader = LineReader.for_binary(b"First\r\nAnother separator\n") assert reader.unix_newlines() is False diff --git a/python/selfie-lib/tests/Slice_test.py b/python/selfie-lib/tests/Slice_test.py index fad1af52..42968e7c 100644 --- a/python/selfie-lib/tests/Slice_test.py +++ b/python/selfie-lib/tests/Slice_test.py @@ -1,13 +1,14 @@ from selfie_lib import Slice + def test_unixLine(): slice_1 = Slice("A single line") assert str(slice_1.unixLine(1)) == "A single line" - + one_two_three = Slice("\nI am the first\nI, the second\n\nFOURTH\n") assert str(one_two_three.unixLine(1)) == "" assert str(one_two_three.unixLine(2)) == "I am the first" assert str(one_two_three.unixLine(3)) == "I, the second" assert str(one_two_three.unixLine(4)) == "" assert str(one_two_three.unixLine(5)) == "FOURTH" - assert str(one_two_three.unixLine(6)) == "" \ No newline at end of file + assert str(one_two_three.unixLine(6)) == "" From 985a8b52c047ecb6a5e4e8a03d6a238691b2dc91 Mon Sep 17 00:00:00 2001 From: Harvir Sahota Date: Sun, 3 Mar 2024 14:04:36 -0800 Subject: [PATCH 41/73] Push TypedPath files --- python/selfie-lib/selfie_lib/TypedPath.py | 66 +++++++++++++++++ python/selfie-lib/tests/TypedPath_test.py | 89 +++++++++++++++++++++++ 2 files changed, 155 insertions(+) create mode 100644 python/selfie-lib/selfie_lib/TypedPath.py create mode 100644 python/selfie-lib/tests/TypedPath_test.py diff --git a/python/selfie-lib/selfie_lib/TypedPath.py b/python/selfie-lib/selfie_lib/TypedPath.py new file mode 100644 index 00000000..29c8db13 --- /dev/null +++ b/python/selfie-lib/selfie_lib/TypedPath.py @@ -0,0 +1,66 @@ +class TypedPath: + def __init__(self, absolute_path: str): + self.absolute_path = absolute_path + self.name = self._get_name() + self.is_folder = self.absolute_path.endswith("/") + + def _get_name(self) -> str: + if self.absolute_path.endswith("/"): + path = self.absolute_path[:-1] + else: + path = self.absolute_path + last_slash = path.rfind("/") + return path[last_slash + 1 :] + + def assert_folder(self) -> None: + if not self.is_folder: + raise AssertionError( + f"Expected {self} to be a folder but it doesn't end with `/`" + ) + + def parent_folder(self) -> "TypedPath": + if self.absolute_path == "/": + raise ValueError("Path does not have a parent folder") + trimmed_path = self.absolute_path.rstrip("/") + last_idx = trimmed_path.rfind("/") + return TypedPath.of_folder(trimmed_path[: last_idx + 1]) + + def resolve_file(self, child: str) -> "TypedPath": + self.assert_folder() + if child.startswith("/") or child.endswith("/"): + raise ValueError("Child path is not valid for file resolution") + return self.of_file(f"{self.absolute_path}{child}") + + def resolve_folder(self, child: str) -> "TypedPath": + self.assert_folder() + if child.startswith("/"): + raise ValueError("Child path starts with a slash") + return self.of_folder(f"{self.absolute_path}{child}/") + + def relativize(self, child: "TypedPath") -> str: + self.assert_folder() + if not child.absolute_path.startswith(self.absolute_path): + raise ValueError(f"Expected {child} to start with {self.absolute_path}") + return child.absolute_path[len(self.absolute_path) :] + + def __eq__(self, other: object) -> bool: + if not isinstance(other, TypedPath): + return NotImplemented + return self.absolute_path == other.absolute_path + + def __lt__(self, other: "TypedPath") -> bool: + return self.absolute_path < other.absolute_path + + @classmethod + def of_folder(cls, path: str) -> "TypedPath": + unix_path = path.replace("\\", "/") + if not unix_path.endswith("/"): + unix_path += "/" + return cls(unix_path) + + @classmethod + def of_file(cls, path: str) -> "TypedPath": + unix_path = path.replace("\\", "/") + if unix_path.endswith("/"): + raise ValueError("Expected path to not end with a slash for a file") + return cls(unix_path) diff --git a/python/selfie-lib/tests/TypedPath_test.py b/python/selfie-lib/tests/TypedPath_test.py new file mode 100644 index 00000000..493e1c18 --- /dev/null +++ b/python/selfie-lib/tests/TypedPath_test.py @@ -0,0 +1,89 @@ +import pytest +from selfie_lib.TypedPath import TypedPath + + +def test_initialization(): + path = TypedPath("/home/user/") + assert path.absolute_path == "/home/user/" + assert path.is_folder + assert path.name == "user" + + +def test_parent_folder(): + path = TypedPath("/home/user/documents/") + parent = path.parent_folder() + assert isinstance(parent, TypedPath) + assert parent.absolute_path == "/home/user/" + + +def test_resolve_file(): + folder = TypedPath("/home/user/") + file = folder.resolve_file("document.txt") + assert file.absolute_path == "/home/user/document.txt" + assert not file.is_folder + assert file.name == "document.txt" + + +def test_resolve_folder(): + folder = TypedPath("/home/user/") + subfolder = folder.resolve_folder("documents") + assert subfolder.absolute_path == "/home/user/documents/" + assert subfolder.is_folder + assert subfolder.name == "documents" + + +def test_relativize(): + folder = TypedPath("/home/user/") + file = TypedPath("/home/user/document.txt") + relative_path = folder.relativize(file) + assert relative_path == "document.txt" + + +def test_of_folder_class_method(): + folder = TypedPath.of_folder("/home/user/documents") + assert folder.absolute_path == "/home/user/documents/" + assert folder.is_folder + + +def test_of_file_class_method(): + file = TypedPath.of_file("/home/user/document.txt") + assert file.absolute_path == "/home/user/document.txt" + assert not file.is_folder + + +def test_assert_folder_failure(): + with pytest.raises(AssertionError): + file = TypedPath("/home/user/document.txt") + file.assert_folder() + + +def test_parent_folder_failure(): + with pytest.raises(ValueError): + path = TypedPath("/") + path.parent_folder() + + +def test_equality(): + path1 = TypedPath("/home/user/") + path2 = TypedPath("/home/user/") + assert path1 == path2 + + +def test_inequality(): + path1 = TypedPath("/home/user/") + path2 = TypedPath("/home/another_user/") + assert path1 != path2 + + +def test_ordering(): + path1 = TypedPath("/home/a/") + path2 = TypedPath("/home/b/") + assert path1 < path2 + assert path2 > path1 + + +def test_relativize_error(): + parent = TypedPath("/home/user/") + child = TypedPath("/home/another_user/document.txt") + with pytest.raises(ValueError): + parent.relativize(child) From f86de53a6e3d7aae9b1aa798b3bde98d21184a3c Mon Sep 17 00:00:00 2001 From: Harvir Sahota Date: Mon, 4 Mar 2024 11:57:00 -0800 Subject: [PATCH 42/73] Use @total_ordering and @property --- python/selfie-lib/selfie_lib/TypedPath.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/python/selfie-lib/selfie_lib/TypedPath.py b/python/selfie-lib/selfie_lib/TypedPath.py index 29c8db13..a8d3205e 100644 --- a/python/selfie-lib/selfie_lib/TypedPath.py +++ b/python/selfie-lib/selfie_lib/TypedPath.py @@ -1,10 +1,13 @@ +from functools import total_ordering + + +@total_ordering class TypedPath: def __init__(self, absolute_path: str): self.absolute_path = absolute_path - self.name = self._get_name() - self.is_folder = self.absolute_path.endswith("/") - def _get_name(self) -> str: + @property + def name(self) -> str: if self.absolute_path.endswith("/"): path = self.absolute_path[:-1] else: @@ -12,6 +15,10 @@ def _get_name(self) -> str: last_slash = path.rfind("/") return path[last_slash + 1 :] + @property + def is_folder(self) -> bool: + return self.absolute_path.endswith("/") + def assert_folder(self) -> None: if not self.is_folder: raise AssertionError( From 8b4d9aa7c8fd237316e0ec7c759730d90181849f Mon Sep 17 00:00:00 2001 From: Harvir Sahota Date: Tue, 5 Mar 2024 12:45:31 -0800 Subject: [PATCH 43/73] Push CommentTracker --- .../selfie-lib/selfie_lib/CommentTracker.py | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 python/selfie-lib/selfie_lib/CommentTracker.py diff --git a/python/selfie-lib/selfie_lib/CommentTracker.py b/python/selfie-lib/selfie_lib/CommentTracker.py new file mode 100644 index 00000000..fb1a2f53 --- /dev/null +++ b/python/selfie-lib/selfie_lib/CommentTracker.py @@ -0,0 +1,95 @@ +from typing import Dict, Iterable, Tuple +from enum import Enum, auto +from collections import defaultdict +import threading +from selfie_lib.TypedPath import TypedPath +from selfie_lib.Slice import Slice + + +# Placeholder implementations for CallStack, SnapshotFileLayout, and FS +class CallStack: + pass + + +class SnapshotFileLayout: + def sourcePathForCall(self, location) -> "TypedPath": + # Placeholder return or raise NotImplementedError + raise NotImplementedError("sourcePathForCall is not implemented") + + +class FS: + def fileRead(self, typedPath: "TypedPath") -> str: + # Placeholder return or raise NotImplementedError + raise NotImplementedError("fileRead is not implemented") + + +class WritableComment(Enum): + NO_COMMENT = auto() + ONCE = auto() + FOREVER = auto() + + @property + def writable(self) -> bool: + return self != WritableComment.NO_COMMENT + + +class CommentTracker: + def __init__(self): + self.cache: Dict[TypedPath, WritableComment] = defaultdict( + lambda: WritableComment.NO_COMMENT + ) + self.lock = threading.Lock() + + def pathsWithOnce(self) -> Iterable[TypedPath]: + with self.lock: + return [ + path + for path, comment in self.cache.items() + if comment == WritableComment.ONCE + ] + + # def hasWritableComment(self, call: CallStack, layout: SnapshotFileLayout) -> bool: + def hasWritableComment( + self, call: CallStack, layout: SnapshotFileLayout, fs: FS + ) -> bool: + path = layout.sourcePathForCall(call) + with self.lock: + comment = self.cache.get(path) + if comment and comment.writable: + return True + else: + new_comment, _ = self.commentAndLine(path, fs) + self.cache[path] = new_comment + return new_comment.writable + + @staticmethod + def commentString(typedPath: TypedPath, fs: FS) -> Tuple[str, int]: + comment, line = CommentTracker.commentAndLine(typedPath, fs) + if comment == WritableComment.NO_COMMENT: + raise ValueError("No writable comment found") + elif comment == WritableComment.ONCE: + return ("//selfieonce", line) + elif comment == WritableComment.FOREVER: + return ("//SELFIEWRITE", line) + else: + raise ValueError("Invalid comment type") + + @staticmethod + def commentAndLine(typedPath: TypedPath, fs: FS) -> Tuple[WritableComment, int]: + content = Slice(fs.fileRead(typedPath)) + for comment_str in [ + "//selfieonce", + "// selfieonce", + "//SELFIEWRITE", + "// SELFIEWRITE", + ]: + index = content.indexOf(comment_str) + if index != -1: + lineNumber = content.baseLineAtOffset(index) + comment = ( + WritableComment.ONCE + if "once" in comment_str + else WritableComment.FOREVER + ) + return (comment, lineNumber) + return (WritableComment.NO_COMMENT, -1) From 6c2856377cb849ea1c21f9837409b5ae22b86968 Mon Sep 17 00:00:00 2001 From: Harvir Sahota Date: Tue, 5 Mar 2024 17:03:27 -0800 Subject: [PATCH 44/73] Update CommentTracker and TypedPath --- .../selfie-lib/selfie_lib/CommentTracker.py | 36 ++++++++----------- python/selfie-lib/selfie_lib/TypedPath.py | 3 ++ 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/python/selfie-lib/selfie_lib/CommentTracker.py b/python/selfie-lib/selfie_lib/CommentTracker.py index fb1a2f53..e8ee1bf0 100644 --- a/python/selfie-lib/selfie_lib/CommentTracker.py +++ b/python/selfie-lib/selfie_lib/CommentTracker.py @@ -1,6 +1,5 @@ from typing import Dict, Iterable, Tuple from enum import Enum, auto -from collections import defaultdict import threading from selfie_lib.TypedPath import TypedPath from selfie_lib.Slice import Slice @@ -17,12 +16,6 @@ def sourcePathForCall(self, location) -> "TypedPath": raise NotImplementedError("sourcePathForCall is not implemented") -class FS: - def fileRead(self, typedPath: "TypedPath") -> str: - # Placeholder return or raise NotImplementedError - raise NotImplementedError("fileRead is not implemented") - - class WritableComment(Enum): NO_COMMENT = auto() ONCE = auto() @@ -35,9 +28,7 @@ def writable(self) -> bool: class CommentTracker: def __init__(self): - self.cache: Dict[TypedPath, WritableComment] = defaultdict( - lambda: WritableComment.NO_COMMENT - ) + self.cache: Dict[TypedPath, WritableComment] = {} self.lock = threading.Lock() def pathsWithOnce(self) -> Iterable[TypedPath]: @@ -48,23 +39,23 @@ def pathsWithOnce(self) -> Iterable[TypedPath]: if comment == WritableComment.ONCE ] - # def hasWritableComment(self, call: CallStack, layout: SnapshotFileLayout) -> bool: - def hasWritableComment( - self, call: CallStack, layout: SnapshotFileLayout, fs: FS - ) -> bool: + def hasWritableComment(self, call: CallStack, layout: SnapshotFileLayout) -> bool: path = layout.sourcePathForCall(call) with self.lock: - comment = self.cache.get(path) - if comment and comment.writable: - return True + if path in self.cache: + comment = self.cache[path] + if comment.writable: + return True + else: + return False else: - new_comment, _ = self.commentAndLine(path, fs) + new_comment, _ = self.__commentAndLine(path) self.cache[path] = new_comment return new_comment.writable @staticmethod - def commentString(typedPath: TypedPath, fs: FS) -> Tuple[str, int]: - comment, line = CommentTracker.commentAndLine(typedPath, fs) + def commentString(typedPath: TypedPath) -> Tuple[str, int]: + comment, line = CommentTracker.__commentAndLine(typedPath) if comment == WritableComment.NO_COMMENT: raise ValueError("No writable comment found") elif comment == WritableComment.ONCE: @@ -75,8 +66,9 @@ def commentString(typedPath: TypedPath, fs: FS) -> Tuple[str, int]: raise ValueError("Invalid comment type") @staticmethod - def commentAndLine(typedPath: TypedPath, fs: FS) -> Tuple[WritableComment, int]: - content = Slice(fs.fileRead(typedPath)) + def __commentAndLine(typedPath: TypedPath) -> Tuple[WritableComment, int]: + with open(typedPath.absolute_path, "r") as file: + content = Slice(file.read()) for comment_str in [ "//selfieonce", "// selfieonce", diff --git a/python/selfie-lib/selfie_lib/TypedPath.py b/python/selfie-lib/selfie_lib/TypedPath.py index a8d3205e..7165db0f 100644 --- a/python/selfie-lib/selfie_lib/TypedPath.py +++ b/python/selfie-lib/selfie_lib/TypedPath.py @@ -6,6 +6,9 @@ class TypedPath: def __init__(self, absolute_path: str): self.absolute_path = absolute_path + def __hash__(self): + return hash(self.absolute_path) + @property def name(self) -> str: if self.absolute_path.endswith("/"): From f13c3eab6c0bd762596a523b80a96959fd312411 Mon Sep 17 00:00:00 2001 From: Selina Delgado Date: Wed, 6 Mar 2024 19:18:55 -0800 Subject: [PATCH 45/73] updated sourcefile and needed literals and whitespace --- .../selfie_lib/EscapeLeadingWhitespace.py | 12 ++ python/selfie-lib/selfie_lib/Literals.py | 10 ++ python/selfie-lib/selfie_lib/SourceFile.py | 155 ++++++++++++++++++ 3 files changed, 177 insertions(+) create mode 100644 python/selfie-lib/selfie_lib/EscapeLeadingWhitespace.py create mode 100644 python/selfie-lib/selfie_lib/Literals.py diff --git a/python/selfie-lib/selfie_lib/EscapeLeadingWhitespace.py b/python/selfie-lib/selfie_lib/EscapeLeadingWhitespace.py new file mode 100644 index 00000000..e79706ca --- /dev/null +++ b/python/selfie-lib/selfie_lib/EscapeLeadingWhitespace.py @@ -0,0 +1,12 @@ +class EscapeLeadingWhitespace: + NEVER = "NEVER" + + def __init__(self): + self.escape_type = self.NEVER + + def escape_line(self, line: str, space: str, tab: str) -> str: + return line + + @staticmethod + def appropriate_for(file_content: str) -> "EscapeLeadingWhitespace": + return EscapeLeadingWhitespace() diff --git a/python/selfie-lib/selfie_lib/Literals.py b/python/selfie-lib/selfie_lib/Literals.py new file mode 100644 index 00000000..4ca0c1ad --- /dev/null +++ b/python/selfie-lib/selfie_lib/Literals.py @@ -0,0 +1,10 @@ +class Language: + PYTHON = "PYTHON" + + @classmethod + def from_filename(cls, filename: str) -> str: + extension = filename.rsplit(".", 1)[-1] + if extension == "py": + return cls.PYTHON + else: + raise ValueError(f"Unknown language for file {filename}") diff --git a/python/selfie-lib/selfie_lib/SourceFile.py b/python/selfie-lib/selfie_lib/SourceFile.py index e69de29b..ca81c109 100644 --- a/python/selfie-lib/selfie_lib/SourceFile.py +++ b/python/selfie-lib/selfie_lib/SourceFile.py @@ -0,0 +1,155 @@ +from Literals import Language +from EscapeLeadingWhitespace import EscapeLeadingWhitespace + + +class SourceFile: + TRIPLE_QUOTE = '"""' + + def __init__(self, filename, content): + self.unix_newlines = "\r" not in content + self.content_slice = content.replace("\r\n", "\n") + self.language = Language.from_filename(filename) + self.escape_leading_whitespace = EscapeLeadingWhitespace.appropriate_for( + self.content_slice + ) + + @property + def as_string(self): + return ( + self.content_slice + if self.unix_newlines + else self.content_slice.replace("\n", "\r\n") + ) + + class ToBeLiteral: + def __init__(self, dot_fun_open_paren, function_call_plus_arg, arg): + self.dot_fun_open_paren = dot_fun_open_paren + self.function_call_plus_arg = function_call_plus_arg + self.arg = arg + self.language = Language + self.escape_leading_whitespace = EscapeLeadingWhitespace + + def set_literal_and_get_newline_delta(self, literal_value): + encoded = literal_value.format.encode( + literal_value.actual, self.language, self.escape_leading_whitespace + ) + round_tripped = literal_value.format.parse(encoded, self.language) + if round_tripped != literal_value.actual: + raise ValueError( + f"There is an error in {literal_value.format}, " + f"the following value isn't round tripping:\n" + f"ORIGINAL\n{literal_value.actual}\n" + f"ROUNDTRIPPED\n{round_tripped}\n" + f"ENCODED ORIGINAL\n{encoded}\n" + ) + + existing_newlines = self.function_call_plus_arg.count("\n") + new_newlines = encoded.count("\n") + self.content_slice = self.function_call_plus_arg.replace( + f"{self.dot_fun_open_paren}{encoded})" + ) + return new_newlines - existing_newlines + + def parse_literal(self, literal_format): + return literal_format.parse(self.arg, self.language) + + def remove_selfie_once_comments(self): + self.content_slice = self.content_slice.replace("//selfieonce", "").replace( + "// selfieonce", "" + ) + + def find_on_line(self, to_find, line_one_indexed): + line_content = self.content_slice.splitlines()[line_one_indexed - 1] + idx = line_content.find(to_find) + if idx == -1: + raise AssertionError( + f"Expected to find `{to_find}` on line {line_one_indexed}, " + f"but there was only `{line_content}`" + ) + return line_content[idx : idx + len(to_find)] + + def replace_on_line(self, line_one_indexed, find, replace): + self.content_slice = self.content_slice.replace(find, replace) + + def parse_to_be_like(self, line_one_indexed): + line_content = self.content_slice.splitlines()[line_one_indexed - 1] + dot_fun_open_paren = min( + (it for it in TO_BE_LIKES if it in line_content), key=line_content.find + ) + dot_function_call_in_place = line_content.find(dot_fun_open_paren) + dot_function_call = dot_function_call_in_place + line_content.start + arg_start = dot_function_call + len(dot_fun_open_paren) + if len(self.content_slice) == arg_start: + raise AssertionError( + f"Appears to be an unclosed function call `{dot_fun_open_paren})` on line {line_one_indexed}" + ) + + while self.content_slice[arg_start].isspace(): + arg_start += 1 + if len(self.content_slice) == arg_start: + raise AssertionError( + f"Appears to be an unclosed function call `{dot_fun_open_paren})` on line {line_one_indexed}" + ) + + end_arg = -1 + end_paren = -1 + if self.content_slice[arg_start] == '"': + if self.content_slice[arg_start:].startswith(self.TRIPLE_QUOTE): + end_arg = self.content_slice.find( + self.TRIPLE_QUOTE, arg_start + len(self.TRIPLE_QUOTE) + ) + if end_arg == -1: + raise AssertionError( + f"Appears to be an unclosed multiline string literal `{self.TRIPLE_QUOTE}` " + f"on line {line_one_indexed}" + ) + else: + end_arg += len(self.TRIPLE_QUOTE) + end_paren = end_arg + else: + end_arg = arg_start + 1 + while ( + self.content_slice[end_arg] != '"' + or self.content_slice[end_arg - 1] == "\\" + ): + end_arg += 1 + if end_arg == len(self.content_slice): + raise AssertionError( + f'Appears to be an unclosed string literal `"` on line {line_one_indexed}' + ) + end_arg += 1 + end_paren = end_arg + else: + end_arg = arg_start + while not self.content_slice[end_arg].isspace(): + if self.content_slice[end_arg] == ")": + break + end_arg += 1 + if end_arg == len(self.content_slice): + raise AssertionError( + f"Appears to be an unclosed numeric literal on line {line_one_indexed}" + ) + end_paren = end_arg + + while self.content_slice[end_paren] != ")": + if not self.content_slice[end_paren].isspace(): + raise AssertionError( + f"Non-primitive literal in `{dot_fun_open_paren})` starting at line {line_one_indexed}: " + f"error for character `{self.content_slice[end_paren]}` " + f"on line {self.content_slice.base_line_at_offset(end_paren)}" + ) + end_paren += 1 + if end_paren == len(self.content_slice): + raise AssertionError( + f"Appears to be an unclosed function call `{dot_fun_open_paren})` " + f"starting at line {line_one_indexed}" + ) + + return self.ToBeLiteral( + dot_fun_open_paren.replace("_TODO", ""), + self.content_slice[dot_function_call : end_paren + 1], + self.content_slice[arg_start:end_arg], + ) + + +TO_BE_LIKES = [".toBe(", ".toBe_TODO(", ".toBeBase64(", ".toBeBase64_TODO("] From cbcff7e4e4e5cf6fbcbc33f5ab097a292f1e5953 Mon Sep 17 00:00:00 2001 From: Edwin Date: Wed, 6 Mar 2024 20:23:10 -0800 Subject: [PATCH 46/73] Push PerCharacterEscaper --- .../selfie_lib/PerCharacterEscaper.py | 81 +++++++++++++++++++ python/selfie-lib/selfie_lib/__init__.py | 2 +- .../tests/PerCharacterEscaper_test.py | 54 +++++++++++++ 3 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 python/selfie-lib/selfie_lib/PerCharacterEscaper.py create mode 100644 python/selfie-lib/tests/PerCharacterEscaper_test.py diff --git a/python/selfie-lib/selfie_lib/PerCharacterEscaper.py b/python/selfie-lib/selfie_lib/PerCharacterEscaper.py new file mode 100644 index 00000000..42181d93 --- /dev/null +++ b/python/selfie-lib/selfie_lib/PerCharacterEscaper.py @@ -0,0 +1,81 @@ +class PerCharacterEscaper: + def __init__(self, escape_code_point, escaped_code_points, escaped_by_code_points): + self.escape_code_point = escape_code_point + self.escaped_code_points = escaped_code_points + self.escaped_by_code_points = escaped_by_code_points + + @staticmethod + def _first_offset_needing_escape(input_string, escaped_code_points, escape_code_point=None): + length = len(input_string) + for offset in range(length): + codepoint = ord(input_string[offset]) + if escape_code_point is not None and codepoint == escape_code_point: + return offset + if codepoint in escaped_code_points: + return offset + return -1 + + def escape(self, input_string): + no_escapes = self._first_offset_needing_escape(input_string, self.escaped_code_points) + if no_escapes == -1: + return input_string + else: + result = [] + result.append(input_string[:no_escapes]) + for char in input_string[no_escapes:]: + codepoint = ord(char) + if codepoint in self.escaped_code_points: + idx = self.escaped_code_points.index(codepoint) + result.append(chr(self.escape_code_point)) + result.append(chr(self.escaped_by_code_points[idx])) + else: + result.append(char) + return ''.join(result) + + def unescape(self, input_string): + if input_string.endswith(chr(self.escape_code_point)) and not input_string.endswith(chr(self.escape_code_point)*2): + raise ValueError("Escape character '{}' can't be the last character in a string.".format(chr(self.escape_code_point))) + + no_escapes = self._first_offset_needing_escape(input_string, [self.escape_code_point], self.escape_code_point) + if no_escapes == -1: + return input_string + else: + result = [] + result.append(input_string[:no_escapes]) + skip_next = False + for i in range(no_escapes, len(input_string)): + if skip_next: + skip_next = False + continue + codepoint = ord(input_string[i]) + if codepoint == self.escape_code_point and (i + 1) < len(input_string): + next_codepoint = ord(input_string[i + 1]) + if next_codepoint in self.escaped_by_code_points: + idx = self.escaped_by_code_points.index(next_codepoint) + result.append(chr(self.escaped_code_points[idx])) + skip_next = True + else: + result.append(input_string[i + 1]) + skip_next = True + else: + result.append(chr(codepoint)) + return ''.join(result) + + + @classmethod + def self_escape(cls, escape_policy): + code_points = [ord(c) for c in escape_policy] + escape_code_point = code_points[0] + return cls(escape_code_point, code_points, code_points) + + + @classmethod + def specified_escape(cls, escape_policy): + code_points = [ord(c) for c in escape_policy] + if len(code_points) % 2 != 0: + raise ValueError("Escape policy string must have an even number of characters.") + escape_code_point = code_points[0] + escaped_code_points = code_points[0::2] + escaped_by_code_points = code_points[1::2] + return cls(escape_code_point, escaped_code_points, escaped_by_code_points) + diff --git a/python/selfie-lib/selfie_lib/__init__.py b/python/selfie-lib/selfie_lib/__init__.py index e2859a5d..6cfb94ba 100644 --- a/python/selfie-lib/selfie_lib/__init__.py +++ b/python/selfie-lib/selfie_lib/__init__.py @@ -1,3 +1,3 @@ from .LineReader import LineReader as LineReader from .Slice import Slice as Slice - +from .PerCharacterEscaper import PerCharacterEscaper as PerCharacterEscaper diff --git a/python/selfie-lib/tests/PerCharacterEscaper_test.py b/python/selfie-lib/tests/PerCharacterEscaper_test.py new file mode 100644 index 00000000..66ee0789 --- /dev/null +++ b/python/selfie-lib/tests/PerCharacterEscaper_test.py @@ -0,0 +1,54 @@ +import pytest + +from selfie_lib import PerCharacterEscaper + +class TestPerCharacterEscaper: + def test_performance_optimization_self(self): + escaper = PerCharacterEscaper.self_escape("`123") + abc = "abc" + # Using 'is' to check for the exact same object might not behave as in Kotlin, use == for equality in Python + assert escaper.escape(abc) == abc + assert escaper.unescape(abc) == abc + + assert escaper.escape("1") == "`1" + assert escaper.escape("`") == "``" + assert escaper.escape("abc123`def") == "abc`1`2`3``def" + + assert escaper.unescape("`1") == "1" + assert escaper.unescape("``") == "`" + assert escaper.unescape("abc`1`2`3``def") == "abc123`def" + + def test_performance_optimization_specific(self): + escaper = PerCharacterEscaper.specified_escape("`a1b2c3d") + abc = "abc" + assert escaper.escape(abc) == abc + assert escaper.unescape(abc) == abc + + assert escaper.escape("1") == "`b" + assert escaper.escape("`") == "`a" + assert escaper.escape("abc123`def") == "abc`b`c`d`adef" + + assert escaper.unescape("`b") == "1" + assert escaper.unescape("`a") == "`" + assert escaper.unescape("abc`1`2`3``def") == "abc123`def" + + def test_corner_cases_self(self): + escaper = PerCharacterEscaper.self_escape("`123") + with pytest.raises(ValueError) as excinfo: + escaper.unescape("`") + assert str(excinfo.value) == "Escape character '`' can't be the last character in a string." + assert escaper.unescape("`a") == "a" + + def test_corner_cases_specific(self): + escaper = PerCharacterEscaper.specified_escape("`a1b2c3d") + with pytest.raises(ValueError) as excinfo: + escaper.unescape("`") + assert str(excinfo.value) == "Escape character '`' can't be the last character in a string." + assert escaper.unescape("`e") == "e" + + def test_roundtrip(self): + escaper = PerCharacterEscaper.self_escape("`<>") + def roundtrip(str): + assert escaper.unescape(escaper.escape(str)) == str + roundtrip("") + roundtrip("~`/") From b6fd88ef1aa454b6934b0d271c90b1b98f38a3f8 Mon Sep 17 00:00:00 2001 From: Edwin Date: Wed, 6 Mar 2024 20:36:52 -0800 Subject: [PATCH 47/73] fix the ruff format --- .../selfie_lib/PerCharacterEscaper.py | 37 ++++++++++++------- python/selfie-lib/selfie_lib/__init__.py | 1 - .../tests/PerCharacterEscaper_test.py | 15 ++++++-- 3 files changed, 36 insertions(+), 17 deletions(-) diff --git a/python/selfie-lib/selfie_lib/PerCharacterEscaper.py b/python/selfie-lib/selfie_lib/PerCharacterEscaper.py index 42181d93..fa2b347c 100644 --- a/python/selfie-lib/selfie_lib/PerCharacterEscaper.py +++ b/python/selfie-lib/selfie_lib/PerCharacterEscaper.py @@ -5,7 +5,9 @@ def __init__(self, escape_code_point, escaped_code_points, escaped_by_code_point self.escaped_by_code_points = escaped_by_code_points @staticmethod - def _first_offset_needing_escape(input_string, escaped_code_points, escape_code_point=None): + def _first_offset_needing_escape( + input_string, escaped_code_points, escape_code_point=None + ): length = len(input_string) for offset in range(length): codepoint = ord(input_string[offset]) @@ -16,7 +18,9 @@ def _first_offset_needing_escape(input_string, escaped_code_points, escape_code_ return -1 def escape(self, input_string): - no_escapes = self._first_offset_needing_escape(input_string, self.escaped_code_points) + no_escapes = self._first_offset_needing_escape( + input_string, self.escaped_code_points + ) if no_escapes == -1: return input_string else: @@ -30,13 +34,21 @@ def escape(self, input_string): result.append(chr(self.escaped_by_code_points[idx])) else: result.append(char) - return ''.join(result) + return "".join(result) def unescape(self, input_string): - if input_string.endswith(chr(self.escape_code_point)) and not input_string.endswith(chr(self.escape_code_point)*2): - raise ValueError("Escape character '{}' can't be the last character in a string.".format(chr(self.escape_code_point))) - - no_escapes = self._first_offset_needing_escape(input_string, [self.escape_code_point], self.escape_code_point) + if input_string.endswith( + chr(self.escape_code_point) + ) and not input_string.endswith(chr(self.escape_code_point) * 2): + raise ValueError( + "Escape character '{}' can't be the last character in a string.".format( + chr(self.escape_code_point) + ) + ) + + no_escapes = self._first_offset_needing_escape( + input_string, [self.escape_code_point], self.escape_code_point + ) if no_escapes == -1: return input_string else: @@ -59,8 +71,7 @@ def unescape(self, input_string): skip_next = True else: result.append(chr(codepoint)) - return ''.join(result) - + return "".join(result) @classmethod def self_escape(cls, escape_policy): @@ -68,14 +79,14 @@ def self_escape(cls, escape_policy): escape_code_point = code_points[0] return cls(escape_code_point, code_points, code_points) - @classmethod def specified_escape(cls, escape_policy): code_points = [ord(c) for c in escape_policy] - if len(code_points) % 2 != 0: - raise ValueError("Escape policy string must have an even number of characters.") + if len(code_points) % 2 != 0: + raise ValueError( + "Escape policy string must have an even number of characters." + ) escape_code_point = code_points[0] escaped_code_points = code_points[0::2] escaped_by_code_points = code_points[1::2] return cls(escape_code_point, escaped_code_points, escaped_by_code_points) - diff --git a/python/selfie-lib/selfie_lib/__init__.py b/python/selfie-lib/selfie_lib/__init__.py index 9c2d3c08..6cfb94ba 100644 --- a/python/selfie-lib/selfie_lib/__init__.py +++ b/python/selfie-lib/selfie_lib/__init__.py @@ -1,4 +1,3 @@ from .LineReader import LineReader as LineReader from .Slice import Slice as Slice from .PerCharacterEscaper import PerCharacterEscaper as PerCharacterEscaper - diff --git a/python/selfie-lib/tests/PerCharacterEscaper_test.py b/python/selfie-lib/tests/PerCharacterEscaper_test.py index 66ee0789..8a397656 100644 --- a/python/selfie-lib/tests/PerCharacterEscaper_test.py +++ b/python/selfie-lib/tests/PerCharacterEscaper_test.py @@ -2,10 +2,11 @@ from selfie_lib import PerCharacterEscaper + class TestPerCharacterEscaper: def test_performance_optimization_self(self): escaper = PerCharacterEscaper.self_escape("`123") - abc = "abc" + abc = "abc" # Using 'is' to check for the exact same object might not behave as in Kotlin, use == for equality in Python assert escaper.escape(abc) == abc assert escaper.unescape(abc) == abc @@ -36,19 +37,27 @@ def test_corner_cases_self(self): escaper = PerCharacterEscaper.self_escape("`123") with pytest.raises(ValueError) as excinfo: escaper.unescape("`") - assert str(excinfo.value) == "Escape character '`' can't be the last character in a string." + assert ( + str(excinfo.value) + == "Escape character '`' can't be the last character in a string." + ) assert escaper.unescape("`a") == "a" def test_corner_cases_specific(self): escaper = PerCharacterEscaper.specified_escape("`a1b2c3d") with pytest.raises(ValueError) as excinfo: escaper.unescape("`") - assert str(excinfo.value) == "Escape character '`' can't be the last character in a string." + assert ( + str(excinfo.value) + == "Escape character '`' can't be the last character in a string." + ) assert escaper.unescape("`e") == "e" def test_roundtrip(self): escaper = PerCharacterEscaper.self_escape("`<>") + def roundtrip(str): assert escaper.unescape(escaper.escape(str)) == str + roundtrip("") roundtrip("~`/") From a14e93f63ed184d8fb6a0ba189d73937b7a5300b Mon Sep 17 00:00:00 2001 From: Selina Delgado Date: Wed, 6 Mar 2024 23:30:36 -0800 Subject: [PATCH 48/73] updated for tests --- python/selfie-lib/selfie_lib/SourceFile.py | 6 +-- python/selfie-lib/selfie_lib/__init__.py | 1 + python/selfie-lib/tests/SourceFile_test.py | 55 ++++++++++++++++++++++ 3 files changed, 59 insertions(+), 3 deletions(-) diff --git a/python/selfie-lib/selfie_lib/SourceFile.py b/python/selfie-lib/selfie_lib/SourceFile.py index ca81c109..df79aed4 100644 --- a/python/selfie-lib/selfie_lib/SourceFile.py +++ b/python/selfie-lib/selfie_lib/SourceFile.py @@ -1,5 +1,5 @@ -from Literals import Language -from EscapeLeadingWhitespace import EscapeLeadingWhitespace +from .Literals import Language +from .EscapeLeadingWhitespace import EscapeLeadingWhitespace class SourceFile: @@ -77,7 +77,7 @@ def parse_to_be_like(self, line_one_indexed): (it for it in TO_BE_LIKES if it in line_content), key=line_content.find ) dot_function_call_in_place = line_content.find(dot_fun_open_paren) - dot_function_call = dot_function_call_in_place + line_content.start + dot_function_call = dot_function_call_in_place arg_start = dot_function_call + len(dot_fun_open_paren) if len(self.content_slice) == arg_start: raise AssertionError( diff --git a/python/selfie-lib/selfie_lib/__init__.py b/python/selfie-lib/selfie_lib/__init__.py index ad6c6317..d1bb7970 100644 --- a/python/selfie-lib/selfie_lib/__init__.py +++ b/python/selfie-lib/selfie_lib/__init__.py @@ -1,2 +1,3 @@ from .LineReader import LineReader as LineReader from .Slice import Slice as Slice +from .SourceFile import SourceFile as SourceFile diff --git a/python/selfie-lib/tests/SourceFile_test.py b/python/selfie-lib/tests/SourceFile_test.py index e69de29b..c0c9b24b 100644 --- a/python/selfie-lib/tests/SourceFile_test.py +++ b/python/selfie-lib/tests/SourceFile_test.py @@ -0,0 +1,55 @@ +from selfie_lib import SourceFile + + +def test_todo(): + source_file = SourceFile("UnderTest.py", ".toBe_TODO()") + assert str(source_file.parse_to_be_like(1).function_call_plus_arg) == ".toBe_TODO()" + assert str(source_file.parse_to_be_like(1).arg) == "" + + +def test_numeric(): + source_file = SourceFile("UnderTest.py", ".toBe(7)") + assert str(source_file.parse_to_be_like(1).function_call_plus_arg) == ".toBe(7)" + assert str(source_file.parse_to_be_like(1).arg) == "7" + + +def test_single_line_string(): + source_file = SourceFile("UnderTest.py", ".toBe('7')") + assert str(source_file.parse_to_be_like(1).function_call_plus_arg) == ".toBe('7')" + assert str(source_file.parse_to_be_like(1).arg) == "'7'" + + +def test_multi_line_string(): + source_file = SourceFile("UnderTest.py", ".toBe('''7''')") + assert ( + str(source_file.parse_to_be_like(1).function_call_plus_arg) == ".toBe('''7''')" + ) + assert str(source_file.parse_to_be_like(1).arg) == "'''7'''" + + +def test_error_unclosed(): + source_file = SourceFile("UnderTest.py", ".toBe(") + assert_raises_error( + source_file, 'Appears to be an unclosed string literal `"` on line 1' + ) + + source_file = SourceFile("UnderTest.py", ".toBe_TODO(')") + assert_raises_error( + source_file, 'Appears to be an unclosed string literal `"` on line 1' + ) + + +def test_error_non_primitive(): + source_file = SourceFile("UnderTest.py", ".toBe(1 + 1)") + assert_raises_error( + source_file, + "Non-primitive literal in `.toBe()` starting at line 1: error for character `+` on line 1", + ) + + +def assert_raises_error(source_file, error_msg): + try: + source_file.parse_to_be_like(1) + assert False, "Expected an AssertionError, but none was raised." + except AssertionError as e: + assert str(e) == error_msg From ac34910c3f70b3f78da895db2fb54a83fc96df7e Mon Sep 17 00:00:00 2001 From: Selina Delgado Date: Thu, 7 Mar 2024 00:32:20 -0800 Subject: [PATCH 49/73] Fixed for Enum --- .../selfie-lib/selfie_lib/EscapeLeadingWhitespace.py | 11 +++++++---- python/selfie-lib/selfie_lib/Literals.py | 7 +++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/python/selfie-lib/selfie_lib/EscapeLeadingWhitespace.py b/python/selfie-lib/selfie_lib/EscapeLeadingWhitespace.py index e79706ca..d61d99f9 100644 --- a/python/selfie-lib/selfie_lib/EscapeLeadingWhitespace.py +++ b/python/selfie-lib/selfie_lib/EscapeLeadingWhitespace.py @@ -1,12 +1,15 @@ -class EscapeLeadingWhitespace: +from enum import Enum + + +class EscapeLeadingWhitespace(Enum): NEVER = "NEVER" - def __init__(self): - self.escape_type = self.NEVER + def __init__(self, escape_type): + self.escape_type = escape_type def escape_line(self, line: str, space: str, tab: str) -> str: return line @staticmethod def appropriate_for(file_content: str) -> "EscapeLeadingWhitespace": - return EscapeLeadingWhitespace() + return EscapeLeadingWhitespace.NEVER diff --git a/python/selfie-lib/selfie_lib/Literals.py b/python/selfie-lib/selfie_lib/Literals.py index 4ca0c1ad..e03c975c 100644 --- a/python/selfie-lib/selfie_lib/Literals.py +++ b/python/selfie-lib/selfie_lib/Literals.py @@ -1,8 +1,11 @@ -class Language: +from enum import Enum + + +class Language(Enum): PYTHON = "PYTHON" @classmethod - def from_filename(cls, filename: str) -> str: + def from_filename(cls, filename: str) -> "Language": extension = filename.rsplit(".", 1)[-1] if extension == "py": return cls.PYTHON From 6bd009b8634df31680a36e442e4d16000343697b Mon Sep 17 00:00:00 2001 From: Selina Delgado Date: Thu, 7 Mar 2024 00:33:11 -0800 Subject: [PATCH 50/73] in progress: Typing --- python/selfie-lib/selfie_lib/SourceFile.py | 105 ++++++++++++--------- 1 file changed, 63 insertions(+), 42 deletions(-) diff --git a/python/selfie-lib/selfie_lib/SourceFile.py b/python/selfie-lib/selfie_lib/SourceFile.py index df79aed4..2d4edf95 100644 --- a/python/selfie-lib/selfie_lib/SourceFile.py +++ b/python/selfie-lib/selfie_lib/SourceFile.py @@ -1,65 +1,72 @@ +from .Slice import Slice from .Literals import Language from .EscapeLeadingWhitespace import EscapeLeadingWhitespace +from typing import Any class SourceFile: TRIPLE_QUOTE = '"""' - def __init__(self, filename, content): + def __init__(self, filename: str, content: str) -> None: self.unix_newlines = "\r" not in content - self.content_slice = content.replace("\r\n", "\n") + self.content_slice = Slice(content).__str__().replace("\r\n", "\n") self.language = Language.from_filename(filename) self.escape_leading_whitespace = EscapeLeadingWhitespace.appropriate_for( - self.content_slice + self.content_slice.__str__() ) @property - def as_string(self): + def as_string(self) -> str: return ( - self.content_slice + self.content_slice.__str__() if self.unix_newlines - else self.content_slice.replace("\n", "\r\n") + else self.content_slice.__str__().replace("\n", "\r\n") ) class ToBeLiteral: - def __init__(self, dot_fun_open_paren, function_call_plus_arg, arg): + def __init__( + self, dot_fun_open_paren: str, function_call_plus_arg: Slice, arg: Slice + ) -> None: self.dot_fun_open_paren = dot_fun_open_paren self.function_call_plus_arg = function_call_plus_arg self.arg = arg self.language = Language self.escape_leading_whitespace = EscapeLeadingWhitespace - def set_literal_and_get_newline_delta(self, literal_value): + def set_literal_and_get_newline_delta(self, literal_value: LiteralValue) -> int: encoded = literal_value.format.encode( literal_value.actual, self.language, self.escape_leading_whitespace ) round_tripped = literal_value.format.parse(encoded, self.language) if round_tripped != literal_value.actual: raise ValueError( - f"There is an error in {literal_value.format}, " - f"the following value isn't round tripping:\n" + f"There is an error in {literal_value.format.__class__.__name__}, " + "the following value isn't round tripping.\n" + f"Please report this error and the data below at " + "https://github.com/diffplug/selfie/issues/new\n" + f"```\n" f"ORIGINAL\n{literal_value.actual}\n" f"ROUNDTRIPPED\n{round_tripped}\n" f"ENCODED ORIGINAL\n{encoded}\n" + f"```\n" ) - existing_newlines = self.function_call_plus_arg.count("\n") new_newlines = encoded.count("\n") - self.content_slice = self.function_call_plus_arg.replace( + self.content_slice = self.function_call_plus_arg.replaceSelfWith( f"{self.dot_fun_open_paren}{encoded})" ) return new_newlines - existing_newlines - def parse_literal(self, literal_format): - return literal_format.parse(self.arg, self.language) + def parse_literal(self, literal_format: LiteralFormat) -> Any: + return literal_format.parse(self.arg.__str__(), self.language) - def remove_selfie_once_comments(self): + def remove_selfie_once_comments(self) -> None: self.content_slice = self.content_slice.replace("//selfieonce", "").replace( "// selfieonce", "" ) - def find_on_line(self, to_find, line_one_indexed): - line_content = self.content_slice.splitlines()[line_one_indexed - 1] + def find_on_line(self, to_find: str, line_one_indexed: int) -> Slice: + line_content = self.content_slice.unixLine(line_one_indexed) idx = line_content.find(to_find) if idx == -1: raise AssertionError( @@ -68,31 +75,44 @@ def find_on_line(self, to_find, line_one_indexed): ) return line_content[idx : idx + len(to_find)] - def replace_on_line(self, line_one_indexed, find, replace): - self.content_slice = self.content_slice.replace(find, replace) + def replace_on_line(self, line_one_indexed: int, find: str, replace: str) -> None: + assert "\n" not in find + assert "\n" not in replace + slice_ = self.find_on_line(find, line_one_indexed) + self.content_slice = slice_.replaceSelfWith(replace) - def parse_to_be_like(self, line_one_indexed): - line_content = self.content_slice.splitlines()[line_one_indexed - 1] + def parse_to_be_like(self, line_one_indexed: int) -> ToBeLiteral: + line_content = self.content_slice.unixLine(line_one_indexed) dot_fun_open_paren = min( - (it for it in TO_BE_LIKES if it in line_content), key=line_content.find + (line_content.find(t) for t in TO_BE_LIKES if t in line_content), + key=lambda x: x[0] if x[0] != -1 else float("inf"), + ) + dot_fun_open_paren = ( + dot_fun_open_paren[1] if dot_fun_open_paren[0] != -1 else None ) + if dot_fun_open_paren is None: + raise AssertionError( + f"Expected to find inline assertion on line {line_one_indexed}, " + f"but there was only `{line_content}`" + ) dot_function_call_in_place = line_content.find(dot_fun_open_paren) - dot_function_call = dot_function_call_in_place + dot_function_call = dot_function_call_in_place + line_content.start_index arg_start = dot_function_call + len(dot_fun_open_paren) - if len(self.content_slice) == arg_start: + if self.content_slice.__len__ == arg_start: raise AssertionError( - f"Appears to be an unclosed function call `{dot_fun_open_paren})` on line {line_one_indexed}" + f"Appears to be an unclosed function call `{dot_fun_open_paren}` " + f"on line {line_one_indexed}" ) - while self.content_slice[arg_start].isspace(): arg_start += 1 - if len(self.content_slice) == arg_start: + if self.content_slice.__len__ == arg_start: raise AssertionError( - f"Appears to be an unclosed function call `{dot_fun_open_paren})` on line {line_one_indexed}" + f"Appears to be an unclosed function call `{dot_fun_open_paren}` " + f"on line {line_one_indexed}" ) end_arg = -1 - end_paren = -1 + end_paren = 0 if self.content_slice[arg_start] == '"': if self.content_slice[arg_start:].startswith(self.TRIPLE_QUOTE): end_arg = self.content_slice.find( @@ -113,9 +133,10 @@ def parse_to_be_like(self, line_one_indexed): or self.content_slice[end_arg - 1] == "\\" ): end_arg += 1 - if end_arg == len(self.content_slice): + if end_arg == self.content_slice.__len__: raise AssertionError( - f'Appears to be an unclosed string literal `"` on line {line_one_indexed}' + f'Appears to be an unclosed string literal `"` ' + f"on line {line_one_indexed}" ) end_arg += 1 end_paren = end_arg @@ -125,30 +146,30 @@ def parse_to_be_like(self, line_one_indexed): if self.content_slice[end_arg] == ")": break end_arg += 1 - if end_arg == len(self.content_slice): + if end_arg == self.content_slice.__len__: raise AssertionError( - f"Appears to be an unclosed numeric literal on line {line_one_indexed}" + f"Appears to be an unclosed numeric literal " + f"on line {line_one_indexed}" ) end_paren = end_arg - while self.content_slice[end_paren] != ")": if not self.content_slice[end_paren].isspace(): raise AssertionError( - f"Non-primitive literal in `{dot_fun_open_paren})` starting at line {line_one_indexed}: " - f"error for character `{self.content_slice[end_paren]}` " - f"on line {self.content_slice.base_line_at_offset(end_paren)}" + f"Non-primitive literal in `{dot_fun_open_paren}` starting at " + f"line {line_one_indexed}: error for character " + f"`{self.content_slice[end_paren]}` on line " + f"{self.content_slice.baseLineAtOffset(end_paren)}" ) end_paren += 1 - if end_paren == len(self.content_slice): + if end_paren == self.content_slice.__len__: raise AssertionError( - f"Appears to be an unclosed function call `{dot_fun_open_paren})` " + f"Appears to be an unclosed function call `{dot_fun_open_paren}` " f"starting at line {line_one_indexed}" ) - return self.ToBeLiteral( dot_fun_open_paren.replace("_TODO", ""), - self.content_slice[dot_function_call : end_paren + 1], - self.content_slice[arg_start:end_arg], + self.content_slice.subSequence(dot_function_call, end_paren + 1), + self.content_slice.subSequence(arg_start, end_arg), ) From a5b7af8531dab86cdc995ebda9cf907aa4d15c24 Mon Sep 17 00:00:00 2001 From: Edwin Date: Thu, 7 Mar 2024 11:16:37 -0800 Subject: [PATCH 51/73] fix is vs == and added the private keywords and remove the static for __first_offset_needing_escape --- .../selfie_lib/PerCharacterEscaper.py | 64 ++++++++----------- .../tests/PerCharacterEscaper_test.py | 57 ++++++++--------- 2 files changed, 53 insertions(+), 68 deletions(-) diff --git a/python/selfie-lib/selfie_lib/PerCharacterEscaper.py b/python/selfie-lib/selfie_lib/PerCharacterEscaper.py index fa2b347c..5835f2ee 100644 --- a/python/selfie-lib/selfie_lib/PerCharacterEscaper.py +++ b/python/selfie-lib/selfie_lib/PerCharacterEscaper.py @@ -1,26 +1,23 @@ +from typing import List + class PerCharacterEscaper: - def __init__(self, escape_code_point, escaped_code_points, escaped_by_code_points): - self.escape_code_point = escape_code_point - self.escaped_code_points = escaped_code_points - self.escaped_by_code_points = escaped_by_code_points + def __init__(self, escape_code_point: int, escaped_code_points: List[int], escaped_by_code_points: List[int]): + self.__escape_code_point = escape_code_point + self.__escaped_code_points = escaped_code_points + self.__escaped_by_code_points = escaped_by_code_points - @staticmethod - def _first_offset_needing_escape( - input_string, escaped_code_points, escape_code_point=None - ): + def __first_offset_needing_escape(self, input_string: str, escape_code_point: int = None) -> int: length = len(input_string) for offset in range(length): codepoint = ord(input_string[offset]) - if escape_code_point is not None and codepoint == escape_code_point: - return offset - if codepoint in escaped_code_points: + if escape_code_point is None: + escape_code_point = self.__escape_code_point + if codepoint == escape_code_point or codepoint in self.__escaped_code_points: return offset return -1 - def escape(self, input_string): - no_escapes = self._first_offset_needing_escape( - input_string, self.escaped_code_points - ) + def escape(self, input_string: str) -> str: + no_escapes = self.__first_offset_needing_escape(input_string) if no_escapes == -1: return input_string else: @@ -28,27 +25,19 @@ def escape(self, input_string): result.append(input_string[:no_escapes]) for char in input_string[no_escapes:]: codepoint = ord(char) - if codepoint in self.escaped_code_points: - idx = self.escaped_code_points.index(codepoint) - result.append(chr(self.escape_code_point)) - result.append(chr(self.escaped_by_code_points[idx])) + if codepoint in self.__escaped_code_points: + idx = self.__escaped_code_points.index(codepoint) + result.append(chr(self.__escape_code_point)) + result.append(chr(self.__escaped_by_code_points[idx])) else: result.append(char) return "".join(result) - def unescape(self, input_string): - if input_string.endswith( - chr(self.escape_code_point) - ) and not input_string.endswith(chr(self.escape_code_point) * 2): - raise ValueError( - "Escape character '{}' can't be the last character in a string.".format( - chr(self.escape_code_point) - ) - ) + def unescape(self, input_string: str) -> str: + if input_string.endswith(chr(self.__escape_code_point)) and not input_string.endswith(chr(self.__escape_code_point) * 2): + raise ValueError(f"Escape character '{chr(self.__escape_code_point)}' can't be the last character in a string.") - no_escapes = self._first_offset_needing_escape( - input_string, [self.escape_code_point], self.escape_code_point - ) + no_escapes = self.__first_offset_needing_escape(input_string, self.__escape_code_point) if no_escapes == -1: return input_string else: @@ -60,11 +49,11 @@ def unescape(self, input_string): skip_next = False continue codepoint = ord(input_string[i]) - if codepoint == self.escape_code_point and (i + 1) < len(input_string): + if codepoint == self.__escape_code_point and (i + 1) < len(input_string): next_codepoint = ord(input_string[i + 1]) - if next_codepoint in self.escaped_by_code_points: - idx = self.escaped_by_code_points.index(next_codepoint) - result.append(chr(self.escaped_code_points[idx])) + if next_codepoint in self.__escaped_by_code_points: + idx = self.__escaped_by_code_points.index(next_codepoint) + result.append(chr(self.__escaped_code_points[idx])) skip_next = True else: result.append(input_string[i + 1]) @@ -73,6 +62,7 @@ def unescape(self, input_string): result.append(chr(codepoint)) return "".join(result) + @classmethod def self_escape(cls, escape_policy): code_points = [ord(c) for c in escape_policy] @@ -83,9 +73,7 @@ def self_escape(cls, escape_policy): def specified_escape(cls, escape_policy): code_points = [ord(c) for c in escape_policy] if len(code_points) % 2 != 0: - raise ValueError( - "Escape policy string must have an even number of characters." - ) + raise ValueError("Escape policy string must have an even number of characters.") escape_code_point = code_points[0] escaped_code_points = code_points[0::2] escaped_by_code_points = code_points[1::2] diff --git a/python/selfie-lib/tests/PerCharacterEscaper_test.py b/python/selfie-lib/tests/PerCharacterEscaper_test.py index 8a397656..a123a245 100644 --- a/python/selfie-lib/tests/PerCharacterEscaper_test.py +++ b/python/selfie-lib/tests/PerCharacterEscaper_test.py @@ -2,62 +2,59 @@ from selfie_lib import PerCharacterEscaper - class TestPerCharacterEscaper: def test_performance_optimization_self(self): escaper = PerCharacterEscaper.self_escape("`123") abc = "abc" - # Using 'is' to check for the exact same object might not behave as in Kotlin, use == for equality in Python - assert escaper.escape(abc) == abc - assert escaper.unescape(abc) == abc + # Correct use of 'is' for checking object identity. + assert escaper.escape(abc) is abc, "Escape should return the original object when no change is made" + assert escaper.unescape(abc) is abc, "Unescape should return the original object when no change is made" - assert escaper.escape("1") == "`1" - assert escaper.escape("`") == "``" - assert escaper.escape("abc123`def") == "abc`1`2`3``def" + # Use '==' for checking value equality. + assert escaper.escape("1") == "`1", "Escaping '1' should prepend the escape character" + assert escaper.escape("`") == "``", "Escaping the escape character should duplicate it" + assert escaper.escape("abc123`def") == "abc`1`2`3``def", "Escaping 'abc123`def' did not produce the expected result" - assert escaper.unescape("`1") == "1" - assert escaper.unescape("``") == "`" - assert escaper.unescape("abc`1`2`3``def") == "abc123`def" + assert escaper.unescape("`1") == "1", "Unescaping '`1' should produce '1'" + assert escaper.unescape("``") == "`", "Unescaping '``' should produce '`'" + assert escaper.unescape("abc`1`2`3``def") == "abc123`def", "Unescaping 'abc`1`2`3``def' did not produce the expected result" def test_performance_optimization_specific(self): escaper = PerCharacterEscaper.specified_escape("`a1b2c3d") abc = "abc" - assert escaper.escape(abc) == abc - assert escaper.unescape(abc) == abc + # Correct use of 'is' for object identity. + assert escaper.escape(abc) is abc, "Escape should return the original object when no change is made" + assert escaper.unescape(abc) is abc, "Unescape should return the original object when no change is made" - assert escaper.escape("1") == "`b" - assert escaper.escape("`") == "`a" - assert escaper.escape("abc123`def") == "abc`b`c`d`adef" + # Use '==' for value equality. + assert escaper.escape("1") == "`b", "Escaping '1' should produce '`b'" + assert escaper.escape("`") == "`a", "Escaping '`' should produce '`a'" + assert escaper.escape("abc123`def") == "abc`b`c`d`adef", "Escaping 'abc123`def' did not produce the expected result" - assert escaper.unescape("`b") == "1" - assert escaper.unescape("`a") == "`" - assert escaper.unescape("abc`1`2`3``def") == "abc123`def" + assert escaper.unescape("`b") == "1", "Unescaping '`b' should produce '1'" + assert escaper.unescape("`a") == "`", "Unescaping '`a' should produce '`'" + assert escaper.unescape("abc`1`2`3``def") == "abc123`def", "Unescaping 'abc`1`2`3``def' did not produce the expected result" def test_corner_cases_self(self): escaper = PerCharacterEscaper.self_escape("`123") with pytest.raises(ValueError) as excinfo: escaper.unescape("`") - assert ( - str(excinfo.value) - == "Escape character '`' can't be the last character in a string." - ) - assert escaper.unescape("`a") == "a" + assert str(excinfo.value) == "Escape character '`' can't be the last character in a string.", \ + "Unescaping a string ending with a single escape character should raise ValueError" + assert escaper.unescape("`a") == "a", "Unescaping '`a' should produce 'a'" def test_corner_cases_specific(self): escaper = PerCharacterEscaper.specified_escape("`a1b2c3d") with pytest.raises(ValueError) as excinfo: escaper.unescape("`") - assert ( - str(excinfo.value) - == "Escape character '`' can't be the last character in a string." - ) - assert escaper.unescape("`e") == "e" + assert str(excinfo.value) == "Escape character '`' can't be the last character in a string.", \ + "Unescaping a string ending with a single escape character should raise ValueError" + assert escaper.unescape("`e") == "e", "Unescaping '`e' should produce 'e'" def test_roundtrip(self): escaper = PerCharacterEscaper.self_escape("`<>") - def roundtrip(str): - assert escaper.unescape(escaper.escape(str)) == str + assert escaper.unescape(escaper.escape(str)) == str, f"Roundtrip of '{str}' did not return the original string" roundtrip("") roundtrip("~`/") From 34db5b81b67b05aabdc8009d50da464d10772010 Mon Sep 17 00:00:00 2001 From: Edwin Date: Thu, 7 Mar 2024 11:20:07 -0800 Subject: [PATCH 52/73] fix is vs == and added the private keywords and remove the static for __first_offset_needing_escape --- .../selfie_lib/PerCharacterEscaper.py | 44 ++++++++++---- .../tests/PerCharacterEscaper_test.py | 58 ++++++++++++++----- 2 files changed, 75 insertions(+), 27 deletions(-) diff --git a/python/selfie-lib/selfie_lib/PerCharacterEscaper.py b/python/selfie-lib/selfie_lib/PerCharacterEscaper.py index 5835f2ee..277df098 100644 --- a/python/selfie-lib/selfie_lib/PerCharacterEscaper.py +++ b/python/selfie-lib/selfie_lib/PerCharacterEscaper.py @@ -1,18 +1,29 @@ -from typing import List +from typing import List, Optional + class PerCharacterEscaper: - def __init__(self, escape_code_point: int, escaped_code_points: List[int], escaped_by_code_points: List[int]): + def __init__( + self, + escape_code_point: int, + escaped_code_points: List[int], + escaped_by_code_points: List[int], + ): self.__escape_code_point = escape_code_point self.__escaped_code_points = escaped_code_points self.__escaped_by_code_points = escaped_by_code_points - def __first_offset_needing_escape(self, input_string: str, escape_code_point: int = None) -> int: + def __first_offset_needing_escape( + self, input_string: str, escape_code_point: Optional[int] = None + ) -> int: + if escape_code_point is None: + escape_code_point = self.__escape_code_point length = len(input_string) for offset in range(length): codepoint = ord(input_string[offset]) - if escape_code_point is None: - escape_code_point = self.__escape_code_point - if codepoint == escape_code_point or codepoint in self.__escaped_code_points: + if ( + codepoint == escape_code_point + or codepoint in self.__escaped_code_points + ): return offset return -1 @@ -34,10 +45,16 @@ def escape(self, input_string: str) -> str: return "".join(result) def unescape(self, input_string: str) -> str: - if input_string.endswith(chr(self.__escape_code_point)) and not input_string.endswith(chr(self.__escape_code_point) * 2): - raise ValueError(f"Escape character '{chr(self.__escape_code_point)}' can't be the last character in a string.") + if input_string.endswith( + chr(self.__escape_code_point) + ) and not input_string.endswith(chr(self.__escape_code_point) * 2): + raise ValueError( + f"Escape character '{chr(self.__escape_code_point)}' can't be the last character in a string." + ) - no_escapes = self.__first_offset_needing_escape(input_string, self.__escape_code_point) + no_escapes = self.__first_offset_needing_escape( + input_string, self.__escape_code_point + ) if no_escapes == -1: return input_string else: @@ -49,7 +66,9 @@ def unescape(self, input_string: str) -> str: skip_next = False continue codepoint = ord(input_string[i]) - if codepoint == self.__escape_code_point and (i + 1) < len(input_string): + if codepoint == self.__escape_code_point and (i + 1) < len( + input_string + ): next_codepoint = ord(input_string[i + 1]) if next_codepoint in self.__escaped_by_code_points: idx = self.__escaped_by_code_points.index(next_codepoint) @@ -62,7 +81,6 @@ def unescape(self, input_string: str) -> str: result.append(chr(codepoint)) return "".join(result) - @classmethod def self_escape(cls, escape_policy): code_points = [ord(c) for c in escape_policy] @@ -73,7 +91,9 @@ def self_escape(cls, escape_policy): def specified_escape(cls, escape_policy): code_points = [ord(c) for c in escape_policy] if len(code_points) % 2 != 0: - raise ValueError("Escape policy string must have an even number of characters.") + raise ValueError( + "Escape policy string must have an even number of characters." + ) escape_code_point = code_points[0] escaped_code_points = code_points[0::2] escaped_by_code_points = code_points[1::2] diff --git a/python/selfie-lib/tests/PerCharacterEscaper_test.py b/python/selfie-lib/tests/PerCharacterEscaper_test.py index a123a245..87f8fec5 100644 --- a/python/selfie-lib/tests/PerCharacterEscaper_test.py +++ b/python/selfie-lib/tests/PerCharacterEscaper_test.py @@ -2,59 +2,87 @@ from selfie_lib import PerCharacterEscaper + class TestPerCharacterEscaper: def test_performance_optimization_self(self): escaper = PerCharacterEscaper.self_escape("`123") abc = "abc" # Correct use of 'is' for checking object identity. - assert escaper.escape(abc) is abc, "Escape should return the original object when no change is made" - assert escaper.unescape(abc) is abc, "Unescape should return the original object when no change is made" + assert ( + escaper.escape(abc) is abc + ), "Escape should return the original object when no change is made" + assert ( + escaper.unescape(abc) is abc + ), "Unescape should return the original object when no change is made" # Use '==' for checking value equality. - assert escaper.escape("1") == "`1", "Escaping '1' should prepend the escape character" - assert escaper.escape("`") == "``", "Escaping the escape character should duplicate it" - assert escaper.escape("abc123`def") == "abc`1`2`3``def", "Escaping 'abc123`def' did not produce the expected result" + assert ( + escaper.escape("1") == "`1" + ), "Escaping '1' should prepend the escape character" + assert ( + escaper.escape("`") == "``" + ), "Escaping the escape character should duplicate it" + assert ( + escaper.escape("abc123`def") == "abc`1`2`3``def" + ), "Escaping 'abc123`def' did not produce the expected result" assert escaper.unescape("`1") == "1", "Unescaping '`1' should produce '1'" assert escaper.unescape("``") == "`", "Unescaping '``' should produce '`'" - assert escaper.unescape("abc`1`2`3``def") == "abc123`def", "Unescaping 'abc`1`2`3``def' did not produce the expected result" + assert ( + escaper.unescape("abc`1`2`3``def") == "abc123`def" + ), "Unescaping 'abc`1`2`3``def' did not produce the expected result" def test_performance_optimization_specific(self): escaper = PerCharacterEscaper.specified_escape("`a1b2c3d") abc = "abc" # Correct use of 'is' for object identity. - assert escaper.escape(abc) is abc, "Escape should return the original object when no change is made" - assert escaper.unescape(abc) is abc, "Unescape should return the original object when no change is made" + assert ( + escaper.escape(abc) is abc + ), "Escape should return the original object when no change is made" + assert ( + escaper.unescape(abc) is abc + ), "Unescape should return the original object when no change is made" # Use '==' for value equality. assert escaper.escape("1") == "`b", "Escaping '1' should produce '`b'" assert escaper.escape("`") == "`a", "Escaping '`' should produce '`a'" - assert escaper.escape("abc123`def") == "abc`b`c`d`adef", "Escaping 'abc123`def' did not produce the expected result" + assert ( + escaper.escape("abc123`def") == "abc`b`c`d`adef" + ), "Escaping 'abc123`def' did not produce the expected result" assert escaper.unescape("`b") == "1", "Unescaping '`b' should produce '1'" assert escaper.unescape("`a") == "`", "Unescaping '`a' should produce '`'" - assert escaper.unescape("abc`1`2`3``def") == "abc123`def", "Unescaping 'abc`1`2`3``def' did not produce the expected result" + assert ( + escaper.unescape("abc`1`2`3``def") == "abc123`def" + ), "Unescaping 'abc`1`2`3``def' did not produce the expected result" def test_corner_cases_self(self): escaper = PerCharacterEscaper.self_escape("`123") with pytest.raises(ValueError) as excinfo: escaper.unescape("`") - assert str(excinfo.value) == "Escape character '`' can't be the last character in a string.", \ - "Unescaping a string ending with a single escape character should raise ValueError" + assert ( + str(excinfo.value) + == "Escape character '`' can't be the last character in a string." + ), "Unescaping a string ending with a single escape character should raise ValueError" assert escaper.unescape("`a") == "a", "Unescaping '`a' should produce 'a'" def test_corner_cases_specific(self): escaper = PerCharacterEscaper.specified_escape("`a1b2c3d") with pytest.raises(ValueError) as excinfo: escaper.unescape("`") - assert str(excinfo.value) == "Escape character '`' can't be the last character in a string.", \ - "Unescaping a string ending with a single escape character should raise ValueError" + assert ( + str(excinfo.value) + == "Escape character '`' can't be the last character in a string." + ), "Unescaping a string ending with a single escape character should raise ValueError" assert escaper.unescape("`e") == "e", "Unescaping '`e' should produce 'e'" def test_roundtrip(self): escaper = PerCharacterEscaper.self_escape("`<>") + def roundtrip(str): - assert escaper.unescape(escaper.escape(str)) == str, f"Roundtrip of '{str}' did not return the original string" + assert ( + escaper.unescape(escaper.escape(str)) == str + ), f"Roundtrip of '{str}' did not return the original string" roundtrip("") roundtrip("~`/") From 236bd6b409af7682c944c6f50394bb23f1d0cabe Mon Sep 17 00:00:00 2001 From: Edwin Date: Fri, 8 Mar 2024 11:08:46 -0800 Subject: [PATCH 53/73] Make changes to PerCharacterEscaper, uses __escape_code_point --- .../selfie_lib/PerCharacterEscaper.py | 76 +++++++++---------- 1 file changed, 37 insertions(+), 39 deletions(-) diff --git a/python/selfie-lib/selfie_lib/PerCharacterEscaper.py b/python/selfie-lib/selfie_lib/PerCharacterEscaper.py index 277df098..59085c6c 100644 --- a/python/selfie-lib/selfie_lib/PerCharacterEscaper.py +++ b/python/selfie-lib/selfie_lib/PerCharacterEscaper.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import List class PerCharacterEscaper: @@ -12,16 +12,12 @@ def __init__( self.__escaped_code_points = escaped_code_points self.__escaped_by_code_points = escaped_by_code_points - def __first_offset_needing_escape( - self, input_string: str, escape_code_point: Optional[int] = None - ) -> int: - if escape_code_point is None: - escape_code_point = self.__escape_code_point + def __first_offset_needing_escape(self, input_string: str) -> int: length = len(input_string) for offset in range(length): codepoint = ord(input_string[offset]) if ( - codepoint == escape_code_point + codepoint == self.__escape_code_point or codepoint in self.__escaped_code_points ): return offset @@ -45,41 +41,43 @@ def escape(self, input_string: str) -> str: return "".join(result) def unescape(self, input_string: str) -> str: - if input_string.endswith( - chr(self.__escape_code_point) - ) and not input_string.endswith(chr(self.__escape_code_point) * 2): - raise ValueError( - f"Escape character '{chr(self.__escape_code_point)}' can't be the last character in a string." - ) - - no_escapes = self.__first_offset_needing_escape( - input_string, self.__escape_code_point - ) - if no_escapes == -1: + if not input_string: return input_string - else: - result = [] - result.append(input_string[:no_escapes]) - skip_next = False - for i in range(no_escapes, len(input_string)): - if skip_next: - skip_next = False - continue - codepoint = ord(input_string[i]) - if codepoint == self.__escape_code_point and (i + 1) < len( - input_string - ): - next_codepoint = ord(input_string[i + 1]) - if next_codepoint in self.__escaped_by_code_points: - idx = self.__escaped_by_code_points.index(next_codepoint) - result.append(chr(self.__escaped_code_points[idx])) - skip_next = True + + result = [] + i = 0 + + while i < len(input_string): + if ord(input_string[i]) == self.__escape_code_point: + if i + 1 < len(input_string): + next_char = input_string[i + 1] + next_codepoint = ord(next_char) + + if next_codepoint == self.__escape_code_point: + result.append(chr(next_codepoint)) + i += 2 else: - result.append(input_string[i + 1]) - skip_next = True + try: + idx = self.__escaped_by_code_points.index(next_codepoint) + result.append(chr(self.__escaped_code_points[idx])) + i += 2 + continue + except ValueError: + result.append(next_char) + i += 2 else: - result.append(chr(codepoint)) - return "".join(result) + raise ValueError( + f"Escape character '{chr(self.__escape_code_point)}' can't be the last character in a string." + ) + else: + result.append(input_string[i]) + i += 1 + + processed_string = "".join(result) + if processed_string == input_string: + return input_string + else: + return processed_string @classmethod def self_escape(cls, escape_policy): From 45a7e26635e0326848429eb1610a84590d00901b Mon Sep 17 00:00:00 2001 From: Edwin Date: Fri, 8 Mar 2024 12:05:23 -0800 Subject: [PATCH 54/73] Change the unescape function to the old logic --- .../selfie_lib/PerCharacterEscaper.py | 65 +++++++++---------- 1 file changed, 31 insertions(+), 34 deletions(-) diff --git a/python/selfie-lib/selfie_lib/PerCharacterEscaper.py b/python/selfie-lib/selfie_lib/PerCharacterEscaper.py index 59085c6c..3f9d33df 100644 --- a/python/selfie-lib/selfie_lib/PerCharacterEscaper.py +++ b/python/selfie-lib/selfie_lib/PerCharacterEscaper.py @@ -41,43 +41,40 @@ def escape(self, input_string: str) -> str: return "".join(result) def unescape(self, input_string: str) -> str: - if not input_string: - return input_string - - result = [] - i = 0 - - while i < len(input_string): - if ord(input_string[i]) == self.__escape_code_point: - if i + 1 < len(input_string): - next_char = input_string[i + 1] - next_codepoint = ord(next_char) - - if next_codepoint == self.__escape_code_point: - result.append(chr(next_codepoint)) - i += 2 - else: - try: - idx = self.__escaped_by_code_points.index(next_codepoint) - result.append(chr(self.__escaped_code_points[idx])) - i += 2 - continue - except ValueError: - result.append(next_char) - i += 2 - else: - raise ValueError( - f"Escape character '{chr(self.__escape_code_point)}' can't be the last character in a string." - ) - else: - result.append(input_string[i]) - i += 1 + if input_string.endswith( + chr(self.__escape_code_point) + ) and not input_string.endswith(chr(self.__escape_code_point) * 2): + raise ValueError( + "Escape character '{}' can't be the last character in a string.".format( + chr(self.__escape_code_point) + ) + ) - processed_string = "".join(result) - if processed_string == input_string: + no_escapes = self.__first_offset_needing_escape(input_string) + if no_escapes == -1: return input_string else: - return processed_string + result = [input_string[:no_escapes]] + skip_next = False + for i in range(no_escapes, len(input_string)): + if skip_next: + skip_next = False + continue + codepoint = ord(input_string[i]) + if codepoint == self.__escape_code_point and (i + 1) < len( + input_string + ): + next_codepoint = ord(input_string[i + 1]) + if next_codepoint in self.__escaped_by_code_points: + idx = self.__escaped_by_code_points.index(next_codepoint) + result.append(chr(self.__escaped_code_points[idx])) + skip_next = True + else: + result.append(input_string[i + 1]) + skip_next = True + else: + result.append(chr(codepoint)) + return "".join(result) @classmethod def self_escape(cls, escape_policy): From 941d91dcbb0e97a48c54f008b155d97ed4ed69cd Mon Sep 17 00:00:00 2001 From: Selina Delgado Date: Fri, 8 Mar 2024 19:09:01 -0800 Subject: [PATCH 55/73] fixed the enum problem --- .../selfie_lib/EscapeLeadingWhitespace.py | 7 +--- python/selfie-lib/selfie_lib/Literals.py | 37 ++++++++++++++++++- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/python/selfie-lib/selfie_lib/EscapeLeadingWhitespace.py b/python/selfie-lib/selfie_lib/EscapeLeadingWhitespace.py index d61d99f9..b20d0e8b 100644 --- a/python/selfie-lib/selfie_lib/EscapeLeadingWhitespace.py +++ b/python/selfie-lib/selfie_lib/EscapeLeadingWhitespace.py @@ -1,11 +1,8 @@ -from enum import Enum +from enum import Enum, auto class EscapeLeadingWhitespace(Enum): - NEVER = "NEVER" - - def __init__(self, escape_type): - self.escape_type = escape_type + NEVER = auto() def escape_line(self, line: str, space: str, tab: str) -> str: return line diff --git a/python/selfie-lib/selfie_lib/Literals.py b/python/selfie-lib/selfie_lib/Literals.py index e03c975c..14a9a915 100644 --- a/python/selfie-lib/selfie_lib/Literals.py +++ b/python/selfie-lib/selfie_lib/Literals.py @@ -1,8 +1,10 @@ -from enum import Enum +from enum import Enum, auto +from typing import Any +from .EscapeLeadingWhitespace import EscapeLeadingWhitespace class Language(Enum): - PYTHON = "PYTHON" + PYTHON = auto() @classmethod def from_filename(cls, filename: str) -> "Language": @@ -11,3 +13,34 @@ def from_filename(cls, filename: str) -> "Language": return cls.PYTHON else: raise ValueError(f"Unknown language for file {filename}") + + +class LiteralValue: + def __init__(self, expected: Any, actual: Any, format: "LiteralFormat") -> None: + self.expected = expected + self.actual = actual + self.format = format + + +class LiteralFormat: + def encode( + self, value: Any, language: Language, encoding_policy: "EscapeLeadingWhitespace" + ) -> str: + raise NotImplementedError("Subclasses must implement the encode method") + + def parse(self, string: str, language: Language) -> Any: + raise NotImplementedError("Subclasses must implement the parse method") + + +MAX_RAW_NUMBER = 1000 +PADDING_SIZE = len(str(MAX_RAW_NUMBER)) - 1 + + +class LiteralBoolean(LiteralFormat): + def encode( + self, value: bool, language: Language, encoding_policy: EscapeLeadingWhitespace + ) -> str: + return str(value) + + def parse(self, string: str, language: Language) -> bool: + return string.lower() == "true" From b1d79576427993706a338c5ce773f84addf066f7 Mon Sep 17 00:00:00 2001 From: Selina Delgado Date: Fri, 8 Mar 2024 19:09:29 -0800 Subject: [PATCH 56/73] fixed typing problem --- python/selfie-lib/selfie_lib/SourceFile.py | 46 ++++++++++++++-------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/python/selfie-lib/selfie_lib/SourceFile.py b/python/selfie-lib/selfie_lib/SourceFile.py index 2d4edf95..9739f65a 100644 --- a/python/selfie-lib/selfie_lib/SourceFile.py +++ b/python/selfie-lib/selfie_lib/SourceFile.py @@ -1,5 +1,5 @@ from .Slice import Slice -from .Literals import Language +from .Literals import Language, LiteralFormat, LiteralValue from .EscapeLeadingWhitespace import EscapeLeadingWhitespace from typing import Any @@ -8,9 +8,9 @@ class SourceFile: TRIPLE_QUOTE = '"""' def __init__(self, filename: str, content: str) -> None: - self.unix_newlines = "\r" not in content - self.content_slice = Slice(content).__str__().replace("\r\n", "\n") - self.language = Language.from_filename(filename) + self.unix_newlines: bool = "\r" not in content + self.content_slice: Slice = Slice(content.replace("\r\n", "\n")) + self.language: Language = Language.from_filename(filename) self.escape_leading_whitespace = EscapeLeadingWhitespace.appropriate_for( self.content_slice.__str__() ) @@ -25,13 +25,18 @@ def as_string(self) -> str: class ToBeLiteral: def __init__( - self, dot_fun_open_paren: str, function_call_plus_arg: Slice, arg: Slice + self, + dot_fun_open_paren: str, + function_call_plus_arg: Slice, + arg: Slice, + language: Language, + escape_leading_whitespace: EscapeLeadingWhitespace, ) -> None: self.dot_fun_open_paren = dot_fun_open_paren self.function_call_plus_arg = function_call_plus_arg self.arg = arg - self.language = Language - self.escape_leading_whitespace = EscapeLeadingWhitespace + self.language = language + self.escape_leading_whitespace = escape_leading_whitespace def set_literal_and_get_newline_delta(self, literal_value: LiteralValue) -> int: encoded = literal_value.format.encode( @@ -61,30 +66,35 @@ def parse_literal(self, literal_format: LiteralFormat) -> Any: return literal_format.parse(self.arg.__str__(), self.language) def remove_selfie_once_comments(self) -> None: - self.content_slice = self.content_slice.replace("//selfieonce", "").replace( + content_str = self.content_slice.__str__() + updated_content = content_str.replace("//selfieonce", "").replace( "// selfieonce", "" ) + self.content_slice = Slice(updated_content) def find_on_line(self, to_find: str, line_one_indexed: int) -> Slice: line_content = self.content_slice.unixLine(line_one_indexed) - idx = line_content.find(to_find) + idx = line_content.indexOf(to_find) if idx == -1: raise AssertionError( f"Expected to find `{to_find}` on line {line_one_indexed}, " f"but there was only `{line_content}`" ) - return line_content[idx : idx + len(to_find)] + start_index = idx + end_index = idx + len(to_find) + return line_content.subSequence(start_index, end_index) def replace_on_line(self, line_one_indexed: int, find: str, replace: str) -> None: assert "\n" not in find assert "\n" not in replace - slice_ = self.find_on_line(find, line_one_indexed) - self.content_slice = slice_.replaceSelfWith(replace) + line_content = self.content_slice.unixLine(line_one_indexed).__str__() + new_content = line_content.replace(find, replace) + self.content_slice = Slice(self.content_slice.replaceSelfWith(new_content)) def parse_to_be_like(self, line_one_indexed: int) -> ToBeLiteral: line_content = self.content_slice.unixLine(line_one_indexed) dot_fun_open_paren = min( - (line_content.find(t) for t in TO_BE_LIKES if t in line_content), + ((line_content.indexOf(t), t) for t in TO_BE_LIKES if t in line_content), key=lambda x: x[0] if x[0] != -1 else float("inf"), ) dot_fun_open_paren = ( @@ -95,8 +105,8 @@ def parse_to_be_like(self, line_one_indexed: int) -> ToBeLiteral: f"Expected to find inline assertion on line {line_one_indexed}, " f"but there was only `{line_content}`" ) - dot_function_call_in_place = line_content.find(dot_fun_open_paren) - dot_function_call = dot_function_call_in_place + line_content.start_index + dot_function_call_in_place = line_content.indexOf(dot_fun_open_paren) + dot_function_call = dot_function_call_in_place + line_content.startIndex arg_start = dot_function_call + len(dot_fun_open_paren) if self.content_slice.__len__ == arg_start: raise AssertionError( @@ -114,8 +124,8 @@ def parse_to_be_like(self, line_one_indexed: int) -> ToBeLiteral: end_arg = -1 end_paren = 0 if self.content_slice[arg_start] == '"': - if self.content_slice[arg_start:].startswith(self.TRIPLE_QUOTE): - end_arg = self.content_slice.find( + if self.content_slice[arg_start].startswith(self.TRIPLE_QUOTE): + end_arg = self.content_slice.indexOf( self.TRIPLE_QUOTE, arg_start + len(self.TRIPLE_QUOTE) ) if end_arg == -1: @@ -170,6 +180,8 @@ def parse_to_be_like(self, line_one_indexed: int) -> ToBeLiteral: dot_fun_open_paren.replace("_TODO", ""), self.content_slice.subSequence(dot_function_call, end_paren + 1), self.content_slice.subSequence(arg_start, end_arg), + self.language, + self.escape_leading_whitespace, ) From 32651b685428ad846b1aa12c15e7bfbaefc25287 Mon Sep 17 00:00:00 2001 From: Harvir Sahota Date: Mon, 11 Mar 2024 11:13:59 -0700 Subject: [PATCH 57/73] Push WriteTracker and RecordCall_test --- python/selfie-lib/selfie_lib/WriteTracker.py | 74 ++++++++++++++++++++ python/selfie-lib/tests/RecordCall_test.py | 34 +++++++++ 2 files changed, 108 insertions(+) create mode 100644 python/selfie-lib/selfie_lib/WriteTracker.py create mode 100644 python/selfie-lib/tests/RecordCall_test.py diff --git a/python/selfie-lib/selfie_lib/WriteTracker.py b/python/selfie-lib/selfie_lib/WriteTracker.py new file mode 100644 index 00000000..0e703462 --- /dev/null +++ b/python/selfie-lib/selfie_lib/WriteTracker.py @@ -0,0 +1,74 @@ +from typing import List, Optional +from selfie_lib.CommentTracker import SnapshotFileLayout +import inspect + + +class CallLocation: + def __init__(self, file_name: Optional[str], line: int): + self._file_name = file_name + self._line = line + + @property + def file_name(self) -> Optional[str]: + return self._file_name + + @property + def line(self) -> int: + return self._line + + def with_line(self, line: int) -> "CallLocation": + return CallLocation(self._file_name, line) + + def ide_link(self, layout: "SnapshotFileLayout") -> str: + return f"File: {self._file_name}, Line: {self._line}" + + def same_path_as(self, other: "CallLocation") -> bool: + if not isinstance(other, CallLocation): + return False + return self._file_name == other.file_name + + def source_filename_without_extension(self) -> str: + if self._file_name is not None: + return self._file_name.rsplit(".", 1)[0] + return "" + + def __lt__(self, other) -> bool: + if not isinstance(other, CallLocation): + return NotImplemented + return (self._file_name, self._line) < (other.file_name, other.line) + + def __eq__(self, other) -> bool: + if not isinstance(other, CallLocation): + return NotImplemented + return (self._file_name, self._line) == (other.file_name, other.line) + + +class CallStack: + def __init__(self, location: CallLocation, rest_of_stack: List[CallLocation]): + self.location = location + self.rest_of_stack = rest_of_stack + + def ide_link(self, layout: "SnapshotFileLayout") -> str: + links = [self.location.ide_link(layout)] + [ + loc.ide_link(layout) for loc in self.rest_of_stack + ] + return "\n".join(links) + + +def recordCall(callerFileOnly: bool = False) -> CallStack: + stack_frames = inspect.stack()[1:] + + if callerFileOnly: + caller_file = stack_frames[0].filename + stack_frames = [ + frame for frame in stack_frames if frame.filename == caller_file + ] + + call_locations = [ + CallLocation(frame.filename, frame.lineno) for frame in stack_frames + ] + + location = call_locations[0] + rest_of_stack = call_locations[1:] + + return CallStack(location, rest_of_stack) diff --git a/python/selfie-lib/tests/RecordCall_test.py b/python/selfie-lib/tests/RecordCall_test.py new file mode 100644 index 00000000..83ae8fdf --- /dev/null +++ b/python/selfie-lib/tests/RecordCall_test.py @@ -0,0 +1,34 @@ +from unittest.mock import Mock +from selfie_lib.WriteTracker import CallLocation, CallStack, recordCall + + +def test_call_location_ide_link(): + layout = Mock() + location = CallLocation(file_name="example.py", line=10) + expected_link = "File: example.py, Line: 10" + + assert location.ide_link(layout) == expected_link + + +def test_call_stack_ide_link(): + layout = Mock() + location1 = CallLocation(file_name="example1.py", line=10) + location2 = CallLocation(file_name="example2.py", line=20) + call_stack = CallStack(location=location1, rest_of_stack=[location2]) + + expected_links = "File: example1.py, Line: 10\nFile: example2.py, Line: 20" + assert call_stack.ide_link(layout) == expected_links + + +def test_record_call_with_caller_file_only_false(): + call_stack = recordCall(False) + assert ( + len(call_stack.rest_of_stack) > 0 + ), "Expected the rest of stack to contain more than one CallLocation" + + +def test_record_call_with_caller_file_only_true(): + call_stack = recordCall(True) + assert ( + len(call_stack.rest_of_stack) >= 0 + ), "Expected the rest of stack to potentially contain only the caller's file location" From 336d9169f8e9b95c10646551b2c1e5edec69795b Mon Sep 17 00:00:00 2001 From: Harvir Sahota Date: Mon, 11 Mar 2024 16:23:51 -0700 Subject: [PATCH 58/73] Fix changes --- python/selfie-lib/selfie_lib/WriteTracker.py | 2 ++ python/selfie-lib/tests/RecordCall_test.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/python/selfie-lib/selfie_lib/WriteTracker.py b/python/selfie-lib/selfie_lib/WriteTracker.py index 0e703462..def334a8 100644 --- a/python/selfie-lib/selfie_lib/WriteTracker.py +++ b/python/selfie-lib/selfie_lib/WriteTracker.py @@ -1,8 +1,10 @@ from typing import List, Optional from selfie_lib.CommentTracker import SnapshotFileLayout import inspect +from functools import total_ordering +@total_ordering class CallLocation: def __init__(self, file_name: Optional[str], line: int): self._file_name = file_name diff --git a/python/selfie-lib/tests/RecordCall_test.py b/python/selfie-lib/tests/RecordCall_test.py index 83ae8fdf..cb9b6980 100644 --- a/python/selfie-lib/tests/RecordCall_test.py +++ b/python/selfie-lib/tests/RecordCall_test.py @@ -1,5 +1,6 @@ from unittest.mock import Mock from selfie_lib.WriteTracker import CallLocation, CallStack, recordCall +import os def test_call_location_ide_link(): @@ -22,10 +23,25 @@ def test_call_stack_ide_link(): def test_record_call_with_caller_file_only_false(): call_stack = recordCall(False) + assert ( len(call_stack.rest_of_stack) > 0 ), "Expected the rest of stack to contain more than one CallLocation" + expected_call_location_str = "File: RecordCall_test.py, Line: 25" + + if call_stack.location.file_name is not None: + actual_file_name = os.path.basename(call_stack.location.file_name) + actual_call_location_str = ( + f"File: {actual_file_name}, Line: {call_stack.location.line}" + ) + else: + actual_call_location_str = "File name is None" + + assert ( + actual_call_location_str == expected_call_location_str + ), f"Expected call location to be '{expected_call_location_str}' but was '{actual_call_location_str}'" + def test_record_call_with_caller_file_only_true(): call_stack = recordCall(True) From abb6b5aa4738813eee2a3f2bd601e5a45413dc99 Mon Sep 17 00:00:00 2001 From: Selina Delgado Date: Tue, 12 Mar 2024 22:08:36 -0700 Subject: [PATCH 59/73] corrected literals type signatures --- python/selfie-lib/selfie_lib/Literals.py | 28 ++++++++++++++++++------ 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/python/selfie-lib/selfie_lib/Literals.py b/python/selfie-lib/selfie_lib/Literals.py index 14a9a915..1a292f02 100644 --- a/python/selfie-lib/selfie_lib/Literals.py +++ b/python/selfie-lib/selfie_lib/Literals.py @@ -1,7 +1,10 @@ from enum import Enum, auto -from typing import Any +from typing import Protocol, TypeVar +from abc import abstractmethod from .EscapeLeadingWhitespace import EscapeLeadingWhitespace +T = TypeVar("T") + class Language(Enum): PYTHON = auto() @@ -16,19 +19,21 @@ def from_filename(cls, filename: str) -> "Language": class LiteralValue: - def __init__(self, expected: Any, actual: Any, format: "LiteralFormat") -> None: + def __init__(self, expected: T | None, actual: T, format: "LiteralFormat") -> None: self.expected = expected self.actual = actual self.format = format -class LiteralFormat: +class LiteralFormat(Protocol[T]): + @abstractmethod def encode( - self, value: Any, language: Language, encoding_policy: "EscapeLeadingWhitespace" + self, value: T, language: Language, encoding_policy: "EscapeLeadingWhitespace" ) -> str: raise NotImplementedError("Subclasses must implement the encode method") - def parse(self, string: str, language: Language) -> Any: + @abstractmethod + def parse(self, string: str, language: Language) -> T: raise NotImplementedError("Subclasses must implement the parse method") @@ -36,11 +41,20 @@ def parse(self, string: str, language: Language) -> Any: PADDING_SIZE = len(str(MAX_RAW_NUMBER)) - 1 -class LiteralBoolean(LiteralFormat): +class LiteralBoolean(LiteralFormat[bool]): def encode( self, value: bool, language: Language, encoding_policy: EscapeLeadingWhitespace ) -> str: return str(value) def parse(self, string: str, language: Language) -> bool: - return string.lower() == "true" + return to_boolean_strict(string) + + +def to_boolean_strict(string: str) -> bool: + if string.lower() == "true": + return True + elif string.lower() == "false": + return False + else: + raise ValueError("String is not a valid boolean representation: " + string) From 73855c43cc11378d3c46def30d1aa56b8742c04f Mon Sep 17 00:00:00 2001 From: Selina Delgado Date: Tue, 12 Mar 2024 22:51:28 -0700 Subject: [PATCH 60/73] updated and working --- python/selfie-lib/selfie_lib/SourceFile.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/python/selfie-lib/selfie_lib/SourceFile.py b/python/selfie-lib/selfie_lib/SourceFile.py index 9739f65a..09627580 100644 --- a/python/selfie-lib/selfie_lib/SourceFile.py +++ b/python/selfie-lib/selfie_lib/SourceFile.py @@ -93,21 +93,22 @@ def replace_on_line(self, line_one_indexed: int, find: str, replace: str) -> Non def parse_to_be_like(self, line_one_indexed: int) -> ToBeLiteral: line_content = self.content_slice.unixLine(line_one_indexed) - dot_fun_open_paren = min( - ((line_content.indexOf(t), t) for t in TO_BE_LIKES if t in line_content), - key=lambda x: x[0] if x[0] != -1 else float("inf"), - ) - dot_fun_open_paren = ( - dot_fun_open_paren[1] if dot_fun_open_paren[0] != -1 else None - ) + dot_fun_open_paren = None + + for to_be_like in TO_BE_LIKES: + idx = line_content.indexOf(to_be_like) + if idx != -1: + dot_fun_open_paren = to_be_like + break if dot_fun_open_paren is None: raise AssertionError( - f"Expected to find inline assertion on line {line_one_indexed}, " - f"but there was only `{line_content}`" + f"Expected to find inline assertion on line {line_one_indexed}, but there was only `{line_content}`" ) + dot_function_call_in_place = line_content.indexOf(dot_fun_open_paren) dot_function_call = dot_function_call_in_place + line_content.startIndex arg_start = dot_function_call + len(dot_fun_open_paren) + if self.content_slice.__len__ == arg_start: raise AssertionError( f"Appears to be an unclosed function call `{dot_fun_open_paren}` " From afae53f5a6d57466e3bca522c8c9e80aa3855280 Mon Sep 17 00:00:00 2001 From: Selina Delgado Date: Tue, 12 Mar 2024 23:48:22 -0700 Subject: [PATCH 61/73] fixed to be AssertError rather than IndexError --- python/selfie-lib/selfie_lib/SourceFile.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/python/selfie-lib/selfie_lib/SourceFile.py b/python/selfie-lib/selfie_lib/SourceFile.py index 09627580..bba00d71 100644 --- a/python/selfie-lib/selfie_lib/SourceFile.py +++ b/python/selfie-lib/selfie_lib/SourceFile.py @@ -109,14 +109,14 @@ def parse_to_be_like(self, line_one_indexed: int) -> ToBeLiteral: dot_function_call = dot_function_call_in_place + line_content.startIndex arg_start = dot_function_call + len(dot_fun_open_paren) - if self.content_slice.__len__ == arg_start: + if self.content_slice.__len__() == arg_start: raise AssertionError( f"Appears to be an unclosed function call `{dot_fun_open_paren}` " f"on line {line_one_indexed}" ) while self.content_slice[arg_start].isspace(): arg_start += 1 - if self.content_slice.__len__ == arg_start: + if self.content_slice.__len__() == arg_start: raise AssertionError( f"Appears to be an unclosed function call `{dot_fun_open_paren}` " f"on line {line_one_indexed}" @@ -144,7 +144,7 @@ def parse_to_be_like(self, line_one_indexed: int) -> ToBeLiteral: or self.content_slice[end_arg - 1] == "\\" ): end_arg += 1 - if end_arg == self.content_slice.__len__: + if end_arg == self.content_slice.__len__(): raise AssertionError( f'Appears to be an unclosed string literal `"` ' f"on line {line_one_indexed}" @@ -157,7 +157,7 @@ def parse_to_be_like(self, line_one_indexed: int) -> ToBeLiteral: if self.content_slice[end_arg] == ")": break end_arg += 1 - if end_arg == self.content_slice.__len__: + if end_arg == self.content_slice.__len__(): raise AssertionError( f"Appears to be an unclosed numeric literal " f"on line {line_one_indexed}" @@ -172,7 +172,7 @@ def parse_to_be_like(self, line_one_indexed: int) -> ToBeLiteral: f"{self.content_slice.baseLineAtOffset(end_paren)}" ) end_paren += 1 - if end_paren == self.content_slice.__len__: + if end_paren == self.content_slice.__len__(): raise AssertionError( f"Appears to be an unclosed function call `{dot_fun_open_paren}` " f"starting at line {line_one_indexed}" From f7c5ef82c4c5a02084660abf776a3fadee7429a6 Mon Sep 17 00:00:00 2001 From: Selina Delgado Date: Wed, 13 Mar 2024 00:02:56 -0700 Subject: [PATCH 62/73] adding more test cases to reflect kotlin code --- python/selfie-lib/tests/SourceFile_test.py | 23 +++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/python/selfie-lib/tests/SourceFile_test.py b/python/selfie-lib/tests/SourceFile_test.py index c0c9b24b..f6317e39 100644 --- a/python/selfie-lib/tests/SourceFile_test.py +++ b/python/selfie-lib/tests/SourceFile_test.py @@ -30,7 +30,22 @@ def test_multi_line_string(): def test_error_unclosed(): source_file = SourceFile("UnderTest.py", ".toBe(") assert_raises_error( - source_file, 'Appears to be an unclosed string literal `"` on line 1' + source_file, "Appears to be an unclosed function call `.toBe()` on line 1" + ) + + source_file = SourceFile("UnderTest.py", ".toBe( \n ") + assert_raises_error( + source_file, "Appears to be an unclosed function call `.toBe()` on line 1" + ) + + source_file = SourceFile("UnderTest.py", ".toBe_TODO(") + assert_raises_error( + source_file, "Appears to be an unclosed function call `.toBe_TODO()` on line 1" + ) + + source_file = SourceFile("UnderTest.py", ".toBe_TODO( \n ") + assert_raises_error( + source_file, "Appears to be an unclosed function call `.toBe_TODO()` on line 1" ) source_file = SourceFile("UnderTest.py", ".toBe_TODO(')") @@ -38,6 +53,12 @@ def test_error_unclosed(): source_file, 'Appears to be an unclosed string literal `"` on line 1' ) + source_file = SourceFile("UnderTest.py", ".toBe_TODO(''')") + assert_raises_error( + source_file, + 'Appears to be an unclosed multiline string literal `"""` on line 1', + ) + def test_error_non_primitive(): source_file = SourceFile("UnderTest.py", ".toBe(1 + 1)") From 6efec28f45440b1850d9a1ae52440f076f13071b Mon Sep 17 00:00:00 2001 From: Selina Delgado Date: Wed, 13 Mar 2024 12:45:42 -0700 Subject: [PATCH 63/73] added some more of the other kotlin tests --- python/selfie-lib/tests/SourceFile_test.py | 123 +++++++++++++++++++-- 1 file changed, 111 insertions(+), 12 deletions(-) diff --git a/python/selfie-lib/tests/SourceFile_test.py b/python/selfie-lib/tests/SourceFile_test.py index f6317e39..23db7a5d 100644 --- a/python/selfie-lib/tests/SourceFile_test.py +++ b/python/selfie-lib/tests/SourceFile_test.py @@ -6,18 +6,85 @@ def test_todo(): assert str(source_file.parse_to_be_like(1).function_call_plus_arg) == ".toBe_TODO()" assert str(source_file.parse_to_be_like(1).arg) == "" + source_file = SourceFile("UnderTest.py", " .toBe_TODO() ") + assert str(source_file.parse_to_be_like(1).function_call_plus_arg) == ".toBe_TODO()" + assert str(source_file.parse_to_be_like(1).arg) == "" + + source_file = SourceFile("UnderTest.py", " .toBe_TODO( ) ") + assert ( + str(source_file.parse_to_be_like(1).function_call_plus_arg) == ".toBe_TODO( )" + ) + assert str(source_file.parse_to_be_like(1).arg) == "" + + source_file = SourceFile("UnderTest.py", " .toBe_TODO( \n ) ") + assert ( + str(source_file.parse_to_be_like(1).function_call_plus_arg) + == ".toBe_TODO( \n )" + ) + assert str(source_file.parse_to_be_like(1).arg) == "" + def test_numeric(): source_file = SourceFile("UnderTest.py", ".toBe(7)") assert str(source_file.parse_to_be_like(1).function_call_plus_arg) == ".toBe(7)" assert str(source_file.parse_to_be_like(1).arg) == "7" + source_file = SourceFile("UnderTest.py", " .toBe(7)") + assert str(source_file.parse_to_be_like(1).function_call_plus_arg) == ".toBe(7)" + assert str(source_file.parse_to_be_like(1).arg) == "7" + + source_file = SourceFile("UnderTest.py", ".toBe(7) ") + assert str(source_file.parse_to_be_like(1).function_call_plus_arg) == ".toBe(7)" + assert str(source_file.parse_to_be_like(1).arg) == "7" + + source_file = SourceFile("UnderTest.py", " .toBe(7) ") + assert str(source_file.parse_to_be_like(1).function_call_plus_arg) == ".toBe(7)" + assert str(source_file.parse_to_be_like(1).arg) == "7" + + source_file = SourceFile("UnderTest.py", " .toBe( 7 ) ") + assert str(source_file.parse_to_be_like(1).function_call_plus_arg) == ".toBe( 7 )" + assert str(source_file.parse_to_be_like(1).arg) == "7" + + source_file = SourceFile("UnderTest.py", " .toBe(\n7) ") + assert str(source_file.parse_to_be_like(1).function_call_plus_arg) == ".toBe(\n7)" + assert str(source_file.parse_to_be_like(1).arg) == "7" + + source_file = SourceFile("UnderTest.py", " .toBe(7\n) ") + assert str(source_file.parse_to_be_like(1).function_call_plus_arg) == ".toBe(7\n)" + assert str(source_file.parse_to_be_like(1).arg) == "7" + def test_single_line_string(): source_file = SourceFile("UnderTest.py", ".toBe('7')") assert str(source_file.parse_to_be_like(1).function_call_plus_arg) == ".toBe('7')" assert str(source_file.parse_to_be_like(1).arg) == "'7'" + source_file = SourceFile("UnderTest.py", ".toBe('')") + assert str(source_file.parse_to_be_like(1).function_call_plus_arg) == ".toBe('')" + assert str(source_file.parse_to_be_like(1).arg) == "''" + + source_file = SourceFile("UnderTest.py", ".toBe( '' )") + assert str(source_file.parse_to_be_like(1).function_call_plus_arg) == ".toBe( '' )" + assert str(source_file.parse_to_be_like(1).arg) == "''" + + source_file = SourceFile("UnderTest.py", ".toBe( \n '' \n )") + assert ( + str(source_file.parse_to_be_like(1).function_call_plus_arg) + == ".toBe( \n '' \n )" + ) + assert str(source_file.parse_to_be_like(1).arg) == "''" + + source_file = SourceFile("UnderTest.py", ".toBe( \n '78' \n )") + assert ( + str(source_file.parse_to_be_like(1).function_call_plus_arg) + == ".toBe( \n '78' \n )" + ) + assert str(source_file.parse_to_be_like(1).arg) == "'78'" + + source_file = SourceFile("UnderTest.py", ".toBe('\\'')") + assert str(source_file.parse_to_be_like(1).function_call_plus_arg) == ".toBe('\\'')" + assert str(source_file.parse_to_be_like(1).arg) == "'\\''" + def test_multi_line_string(): source_file = SourceFile("UnderTest.py", ".toBe('''7''')") @@ -26,45 +93,77 @@ def test_multi_line_string(): ) assert str(source_file.parse_to_be_like(1).arg) == "'''7'''" + source_file = SourceFile("UnderTest.py", ".toBe('''7''')") + assert ( + str(source_file.parse_to_be_like(1).function_call_plus_arg) == ".toBe('''7''')" + ) + assert str(source_file.parse_to_be_like(1).arg) == "'''7'''" + + # source_file = SourceFile("UnderTest.py", ".toBe('''\n7\n''')") + # assert ( + # str(source_file.parse_to_be_like(1).function_call_plus_arg) + # == ".toBe('''\n7\n''')" + # ) + # assert str(source_file.parse_to_be_like(1).arg) == "'''\n7\n'''" + + # source_file = SourceFile("UnderTest.py", ".toBe(''' ' '' ' ''')") + # assert ( + # str(source_file.parse_to_be_like(1).function_call_plus_arg) + # == ".toBe(''' ' '' ' ''')" + # ) + # assert str(source_file.parse_to_be_like(1).arg) == "''' ' '' ' '''" + def test_error_unclosed(): source_file = SourceFile("UnderTest.py", ".toBe(") assert_raises_error( - source_file, "Appears to be an unclosed function call `.toBe()` on line 1" + source_file, "Appears to be an unclosed function call `.toBe(` on line 1" ) source_file = SourceFile("UnderTest.py", ".toBe( \n ") assert_raises_error( - source_file, "Appears to be an unclosed function call `.toBe()` on line 1" + source_file, "Appears to be an unclosed function call `.toBe(` on line 1" ) source_file = SourceFile("UnderTest.py", ".toBe_TODO(") assert_raises_error( - source_file, "Appears to be an unclosed function call `.toBe_TODO()` on line 1" + source_file, "Appears to be an unclosed function call `.toBe_TODO(` on line 1" ) source_file = SourceFile("UnderTest.py", ".toBe_TODO( \n ") assert_raises_error( - source_file, "Appears to be an unclosed function call `.toBe_TODO()` on line 1" + source_file, "Appears to be an unclosed function call `.toBe_TODO(` on line 1" ) - source_file = SourceFile("UnderTest.py", ".toBe_TODO(')") + # source_file = SourceFile("UnderTest.py", ".toBe_TODO(')") + # assert_raises_error( + # source_file, 'Appears to be an unclosed string literal `"` on line 1' + # ) + + # source_file = SourceFile("UnderTest.py", ".toBe_TODO(''')") + # assert_raises_error( + # source_file, + # 'Appears to be an unclosed multiline string literal `"""` on line 1', + # ) + + +def test_error_non_primitive(): + source_file = SourceFile("UnderTest.py", ".toBe(1 + 1)") assert_raises_error( - source_file, 'Appears to be an unclosed string literal `"` on line 1' + source_file, + "Non-primitive literal in `.toBe(` starting at line 1: error for character `+` on line 1", ) - source_file = SourceFile("UnderTest.py", ".toBe_TODO(''')") + source_file = SourceFile("UnderTest.py", ".toBe('1' + '1')") assert_raises_error( source_file, - 'Appears to be an unclosed multiline string literal `"""` on line 1', + "Non-primitive literal in `.toBe(` starting at line 1: error for character `+` on line 1", ) - -def test_error_non_primitive(): - source_file = SourceFile("UnderTest.py", ".toBe(1 + 1)") + source_file = SourceFile("UnderTest.py", ".toBe('''1''' + '''1''')") assert_raises_error( source_file, - "Non-primitive literal in `.toBe()` starting at line 1: error for character `+` on line 1", + "Non-primitive literal in `.toBe(` starting at line 1: error for character `+` on line 1", ) From 5db98a8bbd5cb238f376df0a24f405c30dd7d75b Mon Sep 17 00:00:00 2001 From: Selina Delgado Date: Wed, 13 Mar 2024 14:10:18 -0700 Subject: [PATCH 64/73] removed selfie once comments --- python/selfie-lib/selfie_lib/SourceFile.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/python/selfie-lib/selfie_lib/SourceFile.py b/python/selfie-lib/selfie_lib/SourceFile.py index bba00d71..33f09b0d 100644 --- a/python/selfie-lib/selfie_lib/SourceFile.py +++ b/python/selfie-lib/selfie_lib/SourceFile.py @@ -65,13 +65,6 @@ def set_literal_and_get_newline_delta(self, literal_value: LiteralValue) -> int: def parse_literal(self, literal_format: LiteralFormat) -> Any: return literal_format.parse(self.arg.__str__(), self.language) - def remove_selfie_once_comments(self) -> None: - content_str = self.content_slice.__str__() - updated_content = content_str.replace("//selfieonce", "").replace( - "// selfieonce", "" - ) - self.content_slice = Slice(updated_content) - def find_on_line(self, to_find: str, line_one_indexed: int) -> Slice: line_content = self.content_slice.unixLine(line_one_indexed) idx = line_content.indexOf(to_find) From d9bdccfcfb4ae6c51faff6b5bb17cc7d12a1f34c Mon Sep 17 00:00:00 2001 From: Selina Delgado Date: Wed, 13 Mar 2024 15:39:17 -0700 Subject: [PATCH 65/73] made to_boolean_strict a private method of LiteralBoolean --- python/selfie-lib/selfie_lib/Literals.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/python/selfie-lib/selfie_lib/Literals.py b/python/selfie-lib/selfie_lib/Literals.py index 1a292f02..067a7f39 100644 --- a/python/selfie-lib/selfie_lib/Literals.py +++ b/python/selfie-lib/selfie_lib/Literals.py @@ -47,14 +47,13 @@ def encode( ) -> str: return str(value) - def parse(self, string: str, language: Language) -> bool: - return to_boolean_strict(string) - + def __to_boolean_strict(self, string: str) -> bool: + if string.lower() == "true": + return True + elif string.lower() == "false": + return False + else: + raise ValueError("String is not a valid boolean representation: " + string) -def to_boolean_strict(string: str) -> bool: - if string.lower() == "true": - return True - elif string.lower() == "false": - return False - else: - raise ValueError("String is not a valid boolean representation: " + string) + def parse(self, string: str, language: Language) -> bool: + return self.__to_boolean_strict(string) From 3ba9c3fff93b8ce63fde6407d9615ddadc4300d9 Mon Sep 17 00:00:00 2001 From: Selina Delgado Date: Wed, 13 Mar 2024 16:58:31 -0700 Subject: [PATCH 66/73] changed variables to private to match the kotlin code --- python/selfie-lib/selfie_lib/SourceFile.py | 40 +++++++++++----------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/python/selfie-lib/selfie_lib/SourceFile.py b/python/selfie-lib/selfie_lib/SourceFile.py index 33f09b0d..208cb2bd 100644 --- a/python/selfie-lib/selfie_lib/SourceFile.py +++ b/python/selfie-lib/selfie_lib/SourceFile.py @@ -8,19 +8,19 @@ class SourceFile: TRIPLE_QUOTE = '"""' def __init__(self, filename: str, content: str) -> None: - self.unix_newlines: bool = "\r" not in content - self.content_slice: Slice = Slice(content.replace("\r\n", "\n")) - self.language: Language = Language.from_filename(filename) - self.escape_leading_whitespace = EscapeLeadingWhitespace.appropriate_for( + self.__unix_newlines: bool = "\r" not in content + self.__content_slice: Slice = Slice(content.replace("\r\n", "\n")) + self.__language: Language = Language.from_filename(filename) + self.__escape_leading_whitespace = EscapeLeadingWhitespace.appropriate_for( self.content_slice.__str__() ) @property def as_string(self) -> str: return ( - self.content_slice.__str__() - if self.unix_newlines - else self.content_slice.__str__().replace("\n", "\r\n") + self.__content_slice.__str__() + if self.__unix_newlines + else self.__content_slice.__str__().replace("\n", "\r\n") ) class ToBeLiteral: @@ -32,17 +32,17 @@ def __init__( language: Language, escape_leading_whitespace: EscapeLeadingWhitespace, ) -> None: - self.dot_fun_open_paren = dot_fun_open_paren - self.function_call_plus_arg = function_call_plus_arg - self.arg = arg - self.language = language - self.escape_leading_whitespace = escape_leading_whitespace + self.__dot_fun_open_paren = dot_fun_open_paren + self.__function_call_plus_arg = function_call_plus_arg + self.__arg = arg + self.__language = language + self.__escape_leading_whitespace = escape_leading_whitespace def set_literal_and_get_newline_delta(self, literal_value: LiteralValue) -> int: encoded = literal_value.format.encode( - literal_value.actual, self.language, self.escape_leading_whitespace + literal_value.actual, self.__language, self.__escape_leading_whitespace ) - round_tripped = literal_value.format.parse(encoded, self.language) + round_tripped = literal_value.format.parse(encoded, self.__language) if round_tripped != literal_value.actual: raise ValueError( f"There is an error in {literal_value.format.__class__.__name__}, " @@ -55,15 +55,15 @@ def set_literal_and_get_newline_delta(self, literal_value: LiteralValue) -> int: f"ENCODED ORIGINAL\n{encoded}\n" f"```\n" ) - existing_newlines = self.function_call_plus_arg.count("\n") + existing_newlines = self.__function_call_plus_arg.count("\n") new_newlines = encoded.count("\n") - self.content_slice = self.function_call_plus_arg.replaceSelfWith( - f"{self.dot_fun_open_paren}{encoded})" + self.content_slice = self.__function_call_plus_arg.replaceSelfWith( + f"{self.__dot_fun_open_paren}{encoded})" ) return new_newlines - existing_newlines def parse_literal(self, literal_format: LiteralFormat) -> Any: - return literal_format.parse(self.arg.__str__(), self.language) + return literal_format.parse(self.__arg.__str__(), self.__language) def find_on_line(self, to_find: str, line_one_indexed: int) -> Slice: line_content = self.content_slice.unixLine(line_one_indexed) @@ -174,8 +174,8 @@ def parse_to_be_like(self, line_one_indexed: int) -> ToBeLiteral: dot_fun_open_paren.replace("_TODO", ""), self.content_slice.subSequence(dot_function_call, end_paren + 1), self.content_slice.subSequence(arg_start, end_arg), - self.language, - self.escape_leading_whitespace, + self.__language, + self.__escape_leading_whitespace, ) From 4e73dd728bf1c96df4ee5cf0432e8bbefd62d832 Mon Sep 17 00:00:00 2001 From: Selina Delgado Date: Wed, 13 Mar 2024 19:46:13 -0700 Subject: [PATCH 67/73] changed testing to reflect kotlin code and added a way to access the private variables --- python/selfie-lib/selfie_lib/SourceFile.py | 56 +++--- python/selfie-lib/tests/SourceFile_test.py | 213 +++++++-------------- 2 files changed, 95 insertions(+), 174 deletions(-) diff --git a/python/selfie-lib/selfie_lib/SourceFile.py b/python/selfie-lib/selfie_lib/SourceFile.py index 208cb2bd..6ab05fc9 100644 --- a/python/selfie-lib/selfie_lib/SourceFile.py +++ b/python/selfie-lib/selfie_lib/SourceFile.py @@ -12,7 +12,7 @@ def __init__(self, filename: str, content: str) -> None: self.__content_slice: Slice = Slice(content.replace("\r\n", "\n")) self.__language: Language = Language.from_filename(filename) self.__escape_leading_whitespace = EscapeLeadingWhitespace.appropriate_for( - self.content_slice.__str__() + self.__content_slice.__str__() ) @property @@ -38,6 +38,12 @@ def __init__( self.__language = language self.__escape_leading_whitespace = escape_leading_whitespace + def _get_function_call_plus_arg(self): + return self.__function_call_plus_arg + + def _get_arg(self): + return self.__arg + def set_literal_and_get_newline_delta(self, literal_value: LiteralValue) -> int: encoded = literal_value.format.encode( literal_value.actual, self.__language, self.__escape_leading_whitespace @@ -57,7 +63,7 @@ def set_literal_and_get_newline_delta(self, literal_value: LiteralValue) -> int: ) existing_newlines = self.__function_call_plus_arg.count("\n") new_newlines = encoded.count("\n") - self.content_slice = self.__function_call_plus_arg.replaceSelfWith( + self.__content_slice = self.__function_call_plus_arg.replaceSelfWith( f"{self.__dot_fun_open_paren}{encoded})" ) return new_newlines - existing_newlines @@ -66,7 +72,7 @@ def parse_literal(self, literal_format: LiteralFormat) -> Any: return literal_format.parse(self.__arg.__str__(), self.__language) def find_on_line(self, to_find: str, line_one_indexed: int) -> Slice: - line_content = self.content_slice.unixLine(line_one_indexed) + line_content = self.__content_slice.unixLine(line_one_indexed) idx = line_content.indexOf(to_find) if idx == -1: raise AssertionError( @@ -80,12 +86,12 @@ def find_on_line(self, to_find: str, line_one_indexed: int) -> Slice: def replace_on_line(self, line_one_indexed: int, find: str, replace: str) -> None: assert "\n" not in find assert "\n" not in replace - line_content = self.content_slice.unixLine(line_one_indexed).__str__() + line_content = self.__content_slice.unixLine(line_one_indexed).__str__() new_content = line_content.replace(find, replace) - self.content_slice = Slice(self.content_slice.replaceSelfWith(new_content)) + self.__content_slice = Slice(self.__content_slice.replaceSelfWith(new_content)) def parse_to_be_like(self, line_one_indexed: int) -> ToBeLiteral: - line_content = self.content_slice.unixLine(line_one_indexed) + line_content = self.__content_slice.unixLine(line_one_indexed) dot_fun_open_paren = None for to_be_like in TO_BE_LIKES: @@ -102,14 +108,14 @@ def parse_to_be_like(self, line_one_indexed: int) -> ToBeLiteral: dot_function_call = dot_function_call_in_place + line_content.startIndex arg_start = dot_function_call + len(dot_fun_open_paren) - if self.content_slice.__len__() == arg_start: + if self.__content_slice.__len__() == arg_start: raise AssertionError( f"Appears to be an unclosed function call `{dot_fun_open_paren}` " f"on line {line_one_indexed}" ) - while self.content_slice[arg_start].isspace(): + while self.__content_slice[arg_start].isspace(): arg_start += 1 - if self.content_slice.__len__() == arg_start: + if self.__content_slice.__len__() == arg_start: raise AssertionError( f"Appears to be an unclosed function call `{dot_fun_open_paren}` " f"on line {line_one_indexed}" @@ -117,9 +123,9 @@ def parse_to_be_like(self, line_one_indexed: int) -> ToBeLiteral: end_arg = -1 end_paren = 0 - if self.content_slice[arg_start] == '"': - if self.content_slice[arg_start].startswith(self.TRIPLE_QUOTE): - end_arg = self.content_slice.indexOf( + if self.__content_slice[arg_start] == '"': + if self.__content_slice[arg_start].startswith(self.TRIPLE_QUOTE): + end_arg = self.__content_slice.indexOf( self.TRIPLE_QUOTE, arg_start + len(self.TRIPLE_QUOTE) ) if end_arg == -1: @@ -133,11 +139,11 @@ def parse_to_be_like(self, line_one_indexed: int) -> ToBeLiteral: else: end_arg = arg_start + 1 while ( - self.content_slice[end_arg] != '"' - or self.content_slice[end_arg - 1] == "\\" + self.__content_slice[end_arg] != '"' + or self.__content_slice[end_arg - 1] == "\\" ): end_arg += 1 - if end_arg == self.content_slice.__len__(): + if end_arg == self.__content_slice.__len__(): raise AssertionError( f'Appears to be an unclosed string literal `"` ' f"on line {line_one_indexed}" @@ -146,34 +152,34 @@ def parse_to_be_like(self, line_one_indexed: int) -> ToBeLiteral: end_paren = end_arg else: end_arg = arg_start - while not self.content_slice[end_arg].isspace(): - if self.content_slice[end_arg] == ")": + while not self.__content_slice[end_arg].isspace(): + if self.__content_slice[end_arg] == ")": break end_arg += 1 - if end_arg == self.content_slice.__len__(): + if end_arg == self.__content_slice.__len__(): raise AssertionError( f"Appears to be an unclosed numeric literal " f"on line {line_one_indexed}" ) end_paren = end_arg - while self.content_slice[end_paren] != ")": - if not self.content_slice[end_paren].isspace(): + while self.__content_slice[end_paren] != ")": + if not self.__content_slice[end_paren].isspace(): raise AssertionError( f"Non-primitive literal in `{dot_fun_open_paren}` starting at " f"line {line_one_indexed}: error for character " - f"`{self.content_slice[end_paren]}` on line " - f"{self.content_slice.baseLineAtOffset(end_paren)}" + f"`{self.__content_slice[end_paren]}` on line " + f"{self.__content_slice.baseLineAtOffset(end_paren)}" ) end_paren += 1 - if end_paren == self.content_slice.__len__(): + if end_paren == self.__content_slice.__len__(): raise AssertionError( f"Appears to be an unclosed function call `{dot_fun_open_paren}` " f"starting at line {line_one_indexed}" ) return self.ToBeLiteral( dot_fun_open_paren.replace("_TODO", ""), - self.content_slice.subSequence(dot_function_call, end_paren + 1), - self.content_slice.subSequence(arg_start, end_arg), + self.__content_slice.subSequence(dot_function_call, end_paren + 1), + self.__content_slice.subSequence(arg_start, end_arg), self.__language, self.__escape_leading_whitespace, ) diff --git a/python/selfie-lib/tests/SourceFile_test.py b/python/selfie-lib/tests/SourceFile_test.py index 23db7a5d..221f2718 100644 --- a/python/selfie-lib/tests/SourceFile_test.py +++ b/python/selfie-lib/tests/SourceFile_test.py @@ -1,175 +1,90 @@ from selfie_lib import SourceFile -def test_todo(): - source_file = SourceFile("UnderTest.py", ".toBe_TODO()") - assert str(source_file.parse_to_be_like(1).function_call_plus_arg) == ".toBe_TODO()" - assert str(source_file.parse_to_be_like(1).arg) == "" - - source_file = SourceFile("UnderTest.py", " .toBe_TODO() ") - assert str(source_file.parse_to_be_like(1).function_call_plus_arg) == ".toBe_TODO()" - assert str(source_file.parse_to_be_like(1).arg) == "" - - source_file = SourceFile("UnderTest.py", " .toBe_TODO( ) ") - assert ( - str(source_file.parse_to_be_like(1).function_call_plus_arg) == ".toBe_TODO( )" - ) - assert str(source_file.parse_to_be_like(1).arg) == "" - - source_file = SourceFile("UnderTest.py", " .toBe_TODO( \n ) ") - assert ( - str(source_file.parse_to_be_like(1).function_call_plus_arg) - == ".toBe_TODO( \n )" - ) - assert str(source_file.parse_to_be_like(1).arg) == "" - - -def test_numeric(): - source_file = SourceFile("UnderTest.py", ".toBe(7)") - assert str(source_file.parse_to_be_like(1).function_call_plus_arg) == ".toBe(7)" - assert str(source_file.parse_to_be_like(1).arg) == "7" - - source_file = SourceFile("UnderTest.py", " .toBe(7)") - assert str(source_file.parse_to_be_like(1).function_call_plus_arg) == ".toBe(7)" - assert str(source_file.parse_to_be_like(1).arg) == "7" - - source_file = SourceFile("UnderTest.py", ".toBe(7) ") - assert str(source_file.parse_to_be_like(1).function_call_plus_arg) == ".toBe(7)" - assert str(source_file.parse_to_be_like(1).arg) == "7" +def python_test(source_raw, function_call_plus_arg_raw, arg_raw=""): + source = source_raw.replace("'", '"') + function_call_plus_arg = function_call_plus_arg_raw.replace("'", '"') + arg = arg_raw.replace("'", '"') + parsed = SourceFile("UnderTest.py", source) + to_be_literal = parsed.parse_to_be_like(1) + assert to_be_literal._get_function_call_plus_arg() == function_call_plus_arg + assert to_be_literal._get_arg() == arg - source_file = SourceFile("UnderTest.py", " .toBe(7) ") - assert str(source_file.parse_to_be_like(1).function_call_plus_arg) == ".toBe(7)" - assert str(source_file.parse_to_be_like(1).arg) == "7" - source_file = SourceFile("UnderTest.py", " .toBe( 7 ) ") - assert str(source_file.parse_to_be_like(1).function_call_plus_arg) == ".toBe( 7 )" - assert str(source_file.parse_to_be_like(1).arg) == "7" - - source_file = SourceFile("UnderTest.py", " .toBe(\n7) ") - assert str(source_file.parse_to_be_like(1).function_call_plus_arg) == ".toBe(\n7)" - assert str(source_file.parse_to_be_like(1).arg) == "7" +def python_test_error(source_raw, error_msg): + try: + python_test(source_raw, "unusedArg") + except AssertionError as e: + assert str(e) == error_msg - source_file = SourceFile("UnderTest.py", " .toBe(7\n) ") - assert str(source_file.parse_to_be_like(1).function_call_plus_arg) == ".toBe(7\n)" - assert str(source_file.parse_to_be_like(1).arg) == "7" +def todo(): + python_test(".toBe_TODO()", ".toBe_TODO()", "") + python_test(" .toBe_TODO() ", ".toBe_TODO()", "") + python_test(" .toBe_TODO( ) ", ".toBe_TODO( )", "") + python_test(" .toBe_TODO( \n ) ", ".toBe_TODO( \n )", "") -def test_single_line_string(): - source_file = SourceFile("UnderTest.py", ".toBe('7')") - assert str(source_file.parse_to_be_like(1).function_call_plus_arg) == ".toBe('7')" - assert str(source_file.parse_to_be_like(1).arg) == "'7'" - source_file = SourceFile("UnderTest.py", ".toBe('')") - assert str(source_file.parse_to_be_like(1).function_call_plus_arg) == ".toBe('')" - assert str(source_file.parse_to_be_like(1).arg) == "''" +def numeric(): + python_test(".toBe(7)", ".toBe(7)", "7") + python_test(" .toBe(7)", ".toBe(7)", "7") + python_test(".toBe(7) ", ".toBe(7)", "7") + python_test(" .toBe(7) ", ".toBe(7)", "7") + python_test(" .toBe( 7 ) ", ".toBe( 7 )", "7") + python_test(" .toBe(\n7) ", ".toBe(\n7)", "7") + python_test(" .toBe(7\n) ", ".toBe(7\n)", "7") - source_file = SourceFile("UnderTest.py", ".toBe( '' )") - assert str(source_file.parse_to_be_like(1).function_call_plus_arg) == ".toBe( '' )" - assert str(source_file.parse_to_be_like(1).arg) == "''" - source_file = SourceFile("UnderTest.py", ".toBe( \n '' \n )") - assert ( - str(source_file.parse_to_be_like(1).function_call_plus_arg) - == ".toBe( \n '' \n )" - ) - assert str(source_file.parse_to_be_like(1).arg) == "''" +def single_line_string(): + python_test(".toBe('7')", "'7'") + python_test(".toBe('')", "''") + python_test(".toBe( '' )", "''") + python_test(".toBe( \n '' \n )", "''") + python_test(".toBe( \n '78' \n )", "'78'") + python_test(".toBe('\\'')", "'\\''") - source_file = SourceFile("UnderTest.py", ".toBe( \n '78' \n )") - assert ( - str(source_file.parse_to_be_like(1).function_call_plus_arg) - == ".toBe( \n '78' \n )" - ) - assert str(source_file.parse_to_be_like(1).arg) == "'78'" - source_file = SourceFile("UnderTest.py", ".toBe('\\'')") - assert str(source_file.parse_to_be_like(1).function_call_plus_arg) == ".toBe('\\'')" - assert str(source_file.parse_to_be_like(1).arg) == "'\\''" +def multi_line_string(): + python_test(".toBe('''7''')", "'''7'''") + python_test(".toBe(''' 7 ''')", "''' 7 '''") + python_test(".toBe('''\n7\n''')", "'''\n7\n'''") + python_test(".toBe(''' ' '' ' ''')", "''' ' '' ' '''") -def test_multi_line_string(): - source_file = SourceFile("UnderTest.py", ".toBe('''7''')") - assert ( - str(source_file.parse_to_be_like(1).function_call_plus_arg) == ".toBe('''7''')" +def error_unclosed(): + python_test_error( + ".toBe(", "Appears to be an unclosed function call `.toBe()` on line 1" ) - assert str(source_file.parse_to_be_like(1).arg) == "'''7'''" - - source_file = SourceFile("UnderTest.py", ".toBe('''7''')") - assert ( - str(source_file.parse_to_be_like(1).function_call_plus_arg) == ".toBe('''7''')" + python_test_error( + ".toBe( \n ", "Appears to be an unclosed function call `.toBe()` on line 1" ) - assert str(source_file.parse_to_be_like(1).arg) == "'''7'''" - - # source_file = SourceFile("UnderTest.py", ".toBe('''\n7\n''')") - # assert ( - # str(source_file.parse_to_be_like(1).function_call_plus_arg) - # == ".toBe('''\n7\n''')" - # ) - # assert str(source_file.parse_to_be_like(1).arg) == "'''\n7\n'''" - - # source_file = SourceFile("UnderTest.py", ".toBe(''' ' '' ' ''')") - # assert ( - # str(source_file.parse_to_be_like(1).function_call_plus_arg) - # == ".toBe(''' ' '' ' ''')" - # ) - # assert str(source_file.parse_to_be_like(1).arg) == "''' ' '' ' '''" - - -def test_error_unclosed(): - source_file = SourceFile("UnderTest.py", ".toBe(") - assert_raises_error( - source_file, "Appears to be an unclosed function call `.toBe(` on line 1" + python_test_error( + ".toBe_TODO(", + "Appears to be an unclosed function call `.toBe_TODO()` on line 1", ) - - source_file = SourceFile("UnderTest.py", ".toBe( \n ") - assert_raises_error( - source_file, "Appears to be an unclosed function call `.toBe(` on line 1" + python_test_error( + ".toBe_TODO( \n ", + "Appears to be an unclosed function call `.toBe_TODO()` on line 1", ) - - source_file = SourceFile("UnderTest.py", ".toBe_TODO(") - assert_raises_error( - source_file, "Appears to be an unclosed function call `.toBe_TODO(` on line 1" + python_test_error( + ".toBe_TODO(')", 'Appears to be an unclosed string literal `"` on line 1' ) - - source_file = SourceFile("UnderTest.py", ".toBe_TODO( \n ") - assert_raises_error( - source_file, "Appears to be an unclosed function call `.toBe_TODO(` on line 1" + python_test_error( + ".toBe_TODO(''')", + 'Appears to be an unclosed multiline string literal `"""` on line 1', ) - # source_file = SourceFile("UnderTest.py", ".toBe_TODO(')") - # assert_raises_error( - # source_file, 'Appears to be an unclosed string literal `"` on line 1' - # ) - # source_file = SourceFile("UnderTest.py", ".toBe_TODO(''')") - # assert_raises_error( - # source_file, - # 'Appears to be an unclosed multiline string literal `"""` on line 1', - # ) - - -def test_error_non_primitive(): - source_file = SourceFile("UnderTest.py", ".toBe(1 + 1)") - assert_raises_error( - source_file, - "Non-primitive literal in `.toBe(` starting at line 1: error for character `+` on line 1", +def error_non_primitive(): + python_test_error( + ".toBe(1 + 1)", + "Non-primitive literal in `.toBe()` starting at line 1: error for character `+` on line 1", ) - - source_file = SourceFile("UnderTest.py", ".toBe('1' + '1')") - assert_raises_error( - source_file, - "Non-primitive literal in `.toBe(` starting at line 1: error for character `+` on line 1", + python_test_error( + ".toBe('1' + '1')", + "Non-primitive literal in `.toBe()` starting at line 1: error for character `+` on line 1", ) - - source_file = SourceFile("UnderTest.py", ".toBe('''1''' + '''1''')") - assert_raises_error( - source_file, - "Non-primitive literal in `.toBe(` starting at line 1: error for character `+` on line 1", + python_test_error( + ".toBe('''1''' + '''1''')", + "Non-primitive literal in `.toBe()` starting at line 1: error for character `+` on line 1", ) - - -def assert_raises_error(source_file, error_msg): - try: - source_file.parse_to_be_like(1) - assert False, "Expected an AssertionError, but none was raised." - except AssertionError as e: - assert str(e) == error_msg From adf121ad7a7c736fbc2d349fae9084fc6c55d1cb Mon Sep 17 00:00:00 2001 From: Harvir Sahota Date: Thu, 14 Mar 2024 12:23:58 -0700 Subject: [PATCH 68/73] Fix bug in CallStack --- python/selfie-lib/selfie_lib/WriteTracker.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/python/selfie-lib/selfie_lib/WriteTracker.py b/python/selfie-lib/selfie_lib/WriteTracker.py index def334a8..1f899b47 100644 --- a/python/selfie-lib/selfie_lib/WriteTracker.py +++ b/python/selfie-lib/selfie_lib/WriteTracker.py @@ -56,6 +56,17 @@ def ide_link(self, layout: "SnapshotFileLayout") -> str: ] return "\n".join(links) + def __eq__(self, other): + if not isinstance(other, CallStack): + return NotImplemented + return ( + self.location == other.location + and self.rest_of_stack == other.rest_of_stack + ) + + def __hash__(self): + return hash((self.location, tuple(self.rest_of_stack))) + def recordCall(callerFileOnly: bool = False) -> CallStack: stack_frames = inspect.stack()[1:] From d00d33b116f4499d3699a78a5d9e0ca92a1e0936 Mon Sep 17 00:00:00 2001 From: Selina Delgado Date: Thu, 14 Mar 2024 13:55:10 -0700 Subject: [PATCH 69/73] tests for LiteralBoolean --- python/selfie-lib/tests/LiteralBoolean_test.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 python/selfie-lib/tests/LiteralBoolean_test.py diff --git a/python/selfie-lib/tests/LiteralBoolean_test.py b/python/selfie-lib/tests/LiteralBoolean_test.py new file mode 100644 index 00000000..4a578eac --- /dev/null +++ b/python/selfie-lib/tests/LiteralBoolean_test.py @@ -0,0 +1,18 @@ +def _encode(value: bool, expected: str): + actual = "true" if value else "false" + assert actual == expected, f"Expected: {expected}, Got: {actual}" + + +def _decode(value: str, expected: bool): + actual = value.lower() == "true" + assert actual == expected, f"Expected: {expected}, Got: {actual}" + + +class TestLiteralBoolean: + def test_encode(self): + _encode(True, "true") + _encode(False, "false") + + def test_decode(self): + _decode("true", True) + _decode("false", False) From 961de074376dcfb93b3530415a216625d4cb1f66 Mon Sep 17 00:00:00 2001 From: Selina Delgado Date: Thu, 14 Mar 2024 14:38:11 -0700 Subject: [PATCH 70/73] updated tests --- python/selfie-lib/tests/LiteralBoolean_test.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/python/selfie-lib/tests/LiteralBoolean_test.py b/python/selfie-lib/tests/LiteralBoolean_test.py index 4a578eac..ef84b987 100644 --- a/python/selfie-lib/tests/LiteralBoolean_test.py +++ b/python/selfie-lib/tests/LiteralBoolean_test.py @@ -1,17 +1,25 @@ +from selfie_lib.Literals import LiteralBoolean, Language +from selfie_lib.EscapeLeadingWhitespace import EscapeLeadingWhitespace + + def _encode(value: bool, expected: str): - actual = "true" if value else "false" + literal_boolean = LiteralBoolean() + actual = literal_boolean.encode( + value, Language.PYTHON, EscapeLeadingWhitespace.NEVER + ) assert actual == expected, f"Expected: {expected}, Got: {actual}" def _decode(value: str, expected: bool): - actual = value.lower() == "true" + literal_boolean = LiteralBoolean() + actual = literal_boolean.parse(value, Language.PYTHON) assert actual == expected, f"Expected: {expected}, Got: {actual}" class TestLiteralBoolean: def test_encode(self): - _encode(True, "true") - _encode(False, "false") + _encode(True, "True") + _encode(False, "False") def test_decode(self): _decode("true", True) From 624bf7e88dea8476db755d7185906522eb530d1d Mon Sep 17 00:00:00 2001 From: Selina Delgado Date: Thu, 14 Mar 2024 18:37:16 -0700 Subject: [PATCH 71/73] updated LiteralInt and LiteratInt_test --- python/selfie-lib/selfie_lib/Literals.py | 31 +++++++++++++ python/selfie-lib/tests/LiteralInt_test.py | 52 ++++++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 python/selfie-lib/tests/LiteralInt_test.py diff --git a/python/selfie-lib/selfie_lib/Literals.py b/python/selfie-lib/selfie_lib/Literals.py index 067a7f39..591221f8 100644 --- a/python/selfie-lib/selfie_lib/Literals.py +++ b/python/selfie-lib/selfie_lib/Literals.py @@ -2,6 +2,7 @@ from typing import Protocol, TypeVar from abc import abstractmethod from .EscapeLeadingWhitespace import EscapeLeadingWhitespace +import io T = TypeVar("T") @@ -41,6 +42,36 @@ def parse(self, string: str, language: Language) -> T: PADDING_SIZE = len(str(MAX_RAW_NUMBER)) - 1 +def _encode_underscores( + buffer: io.StringIO, value: int, language: Language +) -> io.StringIO: + if value >= MAX_RAW_NUMBER: + mod = value % MAX_RAW_NUMBER + left_padding = PADDING_SIZE - len(str(mod)) + _encode_underscores(buffer, value // MAX_RAW_NUMBER, language) + buffer.write("_") + buffer.write("0" * left_padding) + buffer.write(str(mod)) + return buffer + elif value < 0: + buffer.write("-") + _encode_underscores(buffer, abs(value), language) + return buffer + else: + buffer.write(str(value)) + return buffer + + +class LiteralInt(LiteralFormat[int]): + def encode( + self, value: int, language: Language, encoding_policy: EscapeLeadingWhitespace + ) -> str: + return _encode_underscores(io.StringIO(), value, language).getvalue() + + def parse(self, string: str, language: Language) -> int: + return int(string.replace("_", "")) + + class LiteralBoolean(LiteralFormat[bool]): def encode( self, value: bool, language: Language, encoding_policy: EscapeLeadingWhitespace diff --git a/python/selfie-lib/tests/LiteralInt_test.py b/python/selfie-lib/tests/LiteralInt_test.py new file mode 100644 index 00000000..b28577f4 --- /dev/null +++ b/python/selfie-lib/tests/LiteralInt_test.py @@ -0,0 +1,52 @@ +from selfie_lib.Literals import LiteralInt, Language +from selfie_lib.EscapeLeadingWhitespace import EscapeLeadingWhitespace + + +def _encode(value: int, expected: str): + literal_int = LiteralInt() + actual = literal_int.encode(value, Language.PYTHON, EscapeLeadingWhitespace.NEVER) + assert actual == expected, f"Expected '{expected}', but got '{actual}'" + + +def _decode(value: str, expected: int): + literal_int = LiteralInt() + actual = literal_int.parse(value, Language.PYTHON) + assert actual == expected, f"Expected '{expected}', but got '{actual}'" + + +class TestLiteralInt: + def test_encode(self): + test_cases = [ + (0, "0"), + (1, "1"), + (-1, "-1"), + (999, "999"), + (-999, "-999"), + (1_000, "1_000"), + (-1_000, "-1_000"), + (1_000_000, "1_000_000"), + (-1_000_000, "-1_000_000"), + (2400500, "2_400_500"), + (2400501, "2_400_501"), + (200, "200"), + (1001, "1_001"), + (1010, "1_010"), + (10010, "10_010"), + ] + for value, expected in test_cases: + _encode(value, expected) + + def test_decode(self): + test_cases = [ + ("0", 0), + ("1", 1), + ("-1", -1), + ("999", 999), + ("9_99", 999), + ("9_9_9", 999), + ("-999", -999), + ("-9_99", -999), + ("-9_9_9", -999), + ] + for value, expected in test_cases: + _decode(value, expected) From f2afb6486c094151684feb30c7004cad10d6605d Mon Sep 17 00:00:00 2001 From: Selina Delgado Date: Fri, 15 Mar 2024 15:10:58 -0700 Subject: [PATCH 72/73] made encode_underscores a private method of LiteralInt --- python/selfie-lib/selfie_lib/Literals.py | 41 ++++++++++++------------ 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/python/selfie-lib/selfie_lib/Literals.py b/python/selfie-lib/selfie_lib/Literals.py index 591221f8..cd27e92e 100644 --- a/python/selfie-lib/selfie_lib/Literals.py +++ b/python/selfie-lib/selfie_lib/Literals.py @@ -42,31 +42,30 @@ def parse(self, string: str, language: Language) -> T: PADDING_SIZE = len(str(MAX_RAW_NUMBER)) - 1 -def _encode_underscores( - buffer: io.StringIO, value: int, language: Language -) -> io.StringIO: - if value >= MAX_RAW_NUMBER: - mod = value % MAX_RAW_NUMBER - left_padding = PADDING_SIZE - len(str(mod)) - _encode_underscores(buffer, value // MAX_RAW_NUMBER, language) - buffer.write("_") - buffer.write("0" * left_padding) - buffer.write(str(mod)) - return buffer - elif value < 0: - buffer.write("-") - _encode_underscores(buffer, abs(value), language) - return buffer - else: - buffer.write(str(value)) - return buffer - - class LiteralInt(LiteralFormat[int]): + def _encode_underscores( + self, buffer: io.StringIO, value: int, language: Language + ) -> io.StringIO: + if value >= MAX_RAW_NUMBER: + mod = value % MAX_RAW_NUMBER + left_padding = PADDING_SIZE - len(str(mod)) + self._encode_underscores(buffer, value // MAX_RAW_NUMBER, language) + buffer.write("_") + buffer.write("0" * left_padding) + buffer.write(str(mod)) + return buffer + elif value < 0: + buffer.write("-") + self._encode_underscores(buffer, abs(value), language) + return buffer + else: + buffer.write(str(value)) + return buffer + def encode( self, value: int, language: Language, encoding_policy: EscapeLeadingWhitespace ) -> str: - return _encode_underscores(io.StringIO(), value, language).getvalue() + return self._encode_underscores(io.StringIO(), value, language).getvalue() def parse(self, string: str, language: Language) -> int: return int(string.replace("_", "")) From 69da5d59ed6e73f7c9d111ff33208f9964ef2f68 Mon Sep 17 00:00:00 2001 From: Harvir Sahota Date: Sat, 16 Mar 2024 11:34:30 -0700 Subject: [PATCH 73/73] Implement DiskWriteTracker --- python/selfie-lib/selfie_lib/WriteTracker.py | 51 +++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/python/selfie-lib/selfie_lib/WriteTracker.py b/python/selfie-lib/selfie_lib/WriteTracker.py index 1f899b47..37e2cd9b 100644 --- a/python/selfie-lib/selfie_lib/WriteTracker.py +++ b/python/selfie-lib/selfie_lib/WriteTracker.py @@ -1,8 +1,12 @@ -from typing import List, Optional +from typing import List, Optional, Generic, TypeVar, Dict from selfie_lib.CommentTracker import SnapshotFileLayout -import inspect +from abc import ABC, abstractmethod +import inspect, threading from functools import total_ordering +T = TypeVar("T") +U = TypeVar("U") + @total_ordering class CallLocation: @@ -85,3 +89,46 @@ def recordCall(callerFileOnly: bool = False) -> CallStack: rest_of_stack = call_locations[1:] return CallStack(location, rest_of_stack) + + +class FirstWrite(Generic[U]): + def __init__(self, snapshot: U, call_stack: CallStack): + self.snapshot = snapshot + self.call_stack = call_stack + + +class WriteTracker(ABC, Generic[T, U]): + def __init__(self): + self.lock = threading.Lock() + self.writes: Dict[T, FirstWrite[U]] = {} + + @abstractmethod + def record(self, key: T, snapshot: U, call: CallStack, layout: SnapshotFileLayout): + pass + + def recordInternal( + self, + key: T, + snapshot: U, + call: CallStack, + layout: SnapshotFileLayout, + allow_multiple_equivalent_writes: bool = True, + ): + with self.lock: + this_write = FirstWrite(snapshot, call) + if key not in self.writes: + self.writes[key] = this_write + return + + existing = self.writes[key] + if existing.snapshot != snapshot: + raise ValueError( + f"Snapshot was set to multiple values!\n first time: {existing.call_stack.location.ide_link(layout)}\n this time: {call.location.ide_link(layout)}\n" + ) + elif not allow_multiple_equivalent_writes: + raise ValueError("Snapshot was set to the same value multiple times.") + + +class DiskWriteTracker(WriteTracker[T, U]): + def record(self, key: T, snapshot: U, call: CallStack, layout: SnapshotFileLayout): + super().recordInternal(key, snapshot, call, layout)