Skip to content

Commit ae3b1a3

Browse files
feat: add __eq__, __bool__, slicing, __reversed__, and infrastructure
- Add __eq__ and __bool__ to all data structures (arrays, stacks, linked lists) - Add slice support (arr[1:3], arr[::-1]) for StaticArray and DynamicArray - Add __reversed__ for DoublyLinkedList (tail-to-head iteration) - Add hypothesis property-based tests (17 invariant checks) - Add doctests to CI pipeline with fixed imports in ABC docstrings - Add coverage badge, CI badge, and Python version badge to README - Add hypothesis dependency and update .gitignore
1 parent c3ddeba commit ae3b1a3

17 files changed

Lines changed: 520 additions & 10 deletions

.github/workflows/ci.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,7 @@ jobs:
4343
run: uv sync --group dev
4444

4545
- name: Run tests
46-
run: uv run pytest tests/ -v
46+
run: uv run pytest tests/ -v --cov-fail-under=90
47+
48+
- name: Run doctests
49+
run: uv run pytest --doctest-modules data_structures/ algorithms/ --no-cov

.gitignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,17 @@ wheels/
88

99
# Virtual environments
1010
.venv
11+
12+
# IDE
1113
.idea/
14+
.vscode/
15+
16+
# Testing / Coverage
1217
.coverage
18+
coverage.json
19+
htmlcov/
20+
.pytest_cache/
21+
.benchmarks/
22+
23+
# Hypothesis
24+
.hypothesis/

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Python DSA
22

3+
![CI](https://github.com/azizjon-aliev/python-dsa/actions/workflows/ci.yml/badge.svg)
4+
![Coverage](https://img.shields.io/badge/coverage-97%25-brightgreen)
5+
![Python](https://img.shields.io/badge/python-3.13%2B-blue)
6+
37
A collection of classic data structures and algorithms implemented in Python.
48

59
## Data Structures
@@ -132,6 +136,9 @@ uv run pytest tests/ -v
132136
# Run benchmarks
133137
uv run pytest tests/test_benchmarks.py --benchmark-enable --no-cov
134138

139+
# Run doctests
140+
uv run pytest --doctest-modules data_structures/ algorithms/ --no-cov
141+
135142
# Lint & format
136143
uv run ruff check .
137144
uv run ruff format .
@@ -169,6 +176,7 @@ tests/
169176
test_linked_lists.py # pytest suite for linked list implementations
170177
test_binary_search.py # pytest suite for binary search
171178
test_benchmarks.py # pytest-benchmark performance tests
179+
test_properties.py # hypothesis property-based tests
172180
.pre-commit-config.yaml # pre-commit hooks (ruff, ty)
173181
pyproject.toml # project config, tools settings
174182
```

data_structures/array/base.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
from abc import ABC, abstractmethod
2-
from typing import Iterator
2+
from typing import Iterator, overload
33

44

55
class IArray[T](ABC):
66
"""Abstract Array interface (random-access, indexed collection).
77
8+
>>> from data_structures.array.static_array import StaticArray
89
>>> a = StaticArray(4)
910
>>> a.append(10); a.append(20); a.append(30)
1011
>>> a[1]
@@ -39,9 +40,13 @@ def clear(self) -> None:
3940
def is_empty(self) -> bool:
4041
"""Return ``True`` if the array contains no elements. O(1)."""
4142

43+
@overload
44+
def __getitem__(self, index: int) -> T: ...
45+
@overload
46+
def __getitem__(self, index: slice) -> list[T]: ...
4247
@abstractmethod
43-
def __getitem__(self, index: int) -> T:
44-
"""Return the element at *index*. O(1)."""
48+
def __getitem__(self, index: int | slice) -> T | list[T]:
49+
"""Return element at *index* or a ``list`` for a slice."""
4550

4651
@abstractmethod
4752
def __setitem__(self, index: int, value: T) -> None:
@@ -59,6 +64,18 @@ def __iter__(self) -> Iterator[T]:
5964
def __contains__(self, item: object) -> bool:
6065
"""Return ``True`` if *item* is in the array. O(n)."""
6166

67+
def __eq__(self, other: object) -> bool:
68+
"""Element-wise equality. Works across array types. O(n)."""
69+
if not isinstance(other, IArray):
70+
return NotImplemented
71+
if len(self) != len(other):
72+
return False
73+
return all(a == b for a, b in zip(self, other))
74+
75+
def __bool__(self) -> bool:
76+
"""Return ``True`` if the array is non-empty."""
77+
return not self.is_empty()
78+
6279
@abstractmethod
6380
def __repr__(self) -> str:
6481
"""Return a developer-friendly representation (e.g. ``StaticArray([1, 2])``)."""

data_structures/array/dynamic_array.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from collections.abc import Iterable
2-
from typing import Iterator
2+
from typing import Iterator, overload
33

44
from .base import IArray
55

@@ -95,8 +95,14 @@ def capacity(self) -> int:
9595
"""Return the current internal capacity."""
9696
return self._capacity
9797

98-
def __getitem__(self, index: int) -> T:
99-
"""Return the element at *index*. O(1)."""
98+
@overload
99+
def __getitem__(self, index: int) -> T: ...
100+
@overload
101+
def __getitem__(self, index: slice) -> list[T]: ...
102+
def __getitem__(self, index: int | slice) -> T | list[T]:
103+
"""Return element at *index* or a ``list`` for a slice."""
104+
if isinstance(index, slice):
105+
return [self._data[i] for i in range(*index.indices(self._size))] # type: ignore[misc]
100106
index = self._normalize_index(index)
101107
return self._data[index] # type: ignore[return-value]
102108

data_structures/array/static_array.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from collections.abc import Iterable
2-
from typing import Iterator
2+
from typing import Iterator, overload
33

44
from .base import IArray
55

@@ -97,8 +97,14 @@ def capacity(self) -> int:
9797
"""Return the maximum number of elements the array can hold."""
9898
return self._capacity
9999

100-
def __getitem__(self, index: int) -> T:
101-
"""Return the element at *index*. O(1)."""
100+
@overload
101+
def __getitem__(self, index: int) -> T: ...
102+
@overload
103+
def __getitem__(self, index: slice) -> list[T]: ...
104+
def __getitem__(self, index: int | slice) -> T | list[T]:
105+
"""Return element at *index* or a ``list`` for a slice."""
106+
if isinstance(index, slice):
107+
return [self._data[i] for i in range(*index.indices(self._size))] # type: ignore[misc]
102108
index = self._normalize_index(index)
103109
return self._data[index] # type: ignore[return-value]
104110

data_structures/linked_list/base.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,18 @@ def __len__(self) -> int:
4747
def __contains__(self, item: object) -> bool:
4848
"""Return ``True`` if *item* is in the list. O(n)."""
4949

50+
def __eq__(self, other: object) -> bool:
51+
"""Element-wise equality. Works across list types. O(n)."""
52+
if not isinstance(other, ISinglyLinkedList):
53+
return NotImplemented
54+
if len(self) != len(other):
55+
return False
56+
return all(a == b for a, b in zip(self, other))
57+
58+
def __bool__(self) -> bool:
59+
"""Return ``True`` if the list is non-empty."""
60+
return not self.is_empty()
61+
5062
@abstractmethod
5163
def __repr__(self) -> str:
5264
"""Return a developer-friendly representation."""
@@ -74,3 +86,7 @@ def pop_back(self) -> T:
7486
@abstractmethod
7587
def peek_back(self) -> T:
7688
"""Return the tail element without removing it. O(1)."""
89+
90+
@abstractmethod
91+
def __reversed__(self) -> Iterator[T]:
92+
"""Iterate over all elements from tail to head. O(n)."""

data_structures/linked_list/doubly_linked_list.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,12 @@ def __iter__(self) -> Iterator[T]:
8989
yield node.value
9090
node = node.next
9191

92+
def __reversed__(self) -> Iterator[T]:
93+
node = self._tail
94+
while node is not None:
95+
yield node.value
96+
node = node.prev
97+
9298
def __len__(self) -> int:
9399
return self._size
94100

data_structures/stack/array_stack.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Iterator
2+
13
from data_structures.array.dynamic_array import DynamicArray
24

35
from .base import IStack, StackEmptyError
@@ -34,6 +36,9 @@ def __len__(self) -> int:
3436
def __contains__(self, item: object) -> bool:
3537
return item in self._items
3638

39+
def _iter_items(self) -> Iterator[T]:
40+
return reversed(list(self._items))
41+
3742
def __repr__(self) -> str:
3843
return f"{type(self).__name__}({list(self._items)})"
3944

data_structures/stack/base.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from abc import ABC, abstractmethod
2+
from typing import Iterator
23

34

45
class StackEmptyError(Exception):
@@ -11,6 +12,7 @@ def __init__(self) -> None:
1112
class IStack[T](ABC):
1213
"""Abstract Stack interface (LIFO - Last In First Out).
1314
15+
>>> from data_structures.stack.array_stack import ArrayStack
1416
>>> s = ArrayStack()
1517
>>> s.push(1); s.push(2); s.push(3)
1618
>>> s.peek()
@@ -49,6 +51,22 @@ def __len__(self) -> int:
4951
def __contains__(self, item: object) -> bool:
5052
"""Return ``True`` if *item* is in the stack. O(n)."""
5153

54+
def __eq__(self, other: object) -> bool:
55+
"""Element-wise equality (top to bottom). O(n)."""
56+
if not isinstance(other, IStack):
57+
return NotImplemented
58+
if len(self) != len(other):
59+
return False
60+
return list(self._iter_items()) == list(other._iter_items())
61+
62+
def __bool__(self) -> bool:
63+
"""Return ``True`` if the stack is non-empty."""
64+
return not self.is_empty()
65+
66+
@abstractmethod
67+
def _iter_items(self) -> Iterator[T]:
68+
"""Iterate elements from top to bottom (for equality)."""
69+
5270
@abstractmethod
5371
def __repr__(self) -> str:
5472
"""Return a developer-friendly representation."""

0 commit comments

Comments
 (0)