From e4d347c1aef9e427beb22ed335658236d850c099 Mon Sep 17 00:00:00 2001 From: Harvir Sahota Date: Thu, 29 Feb 2024 21:47:27 -0800 Subject: [PATCH 1/3] 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 2/3] 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 3/3] 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(