Skip to content

Commit fc74bb0

Browse files
committed
wip: Improve documentation
TODO: Issues with pdoc not understanding markdown reference links TODO: Deploy to github-pages
1 parent d3b2d99 commit fc74bb0

File tree

3 files changed

+128
-25
lines changed

3 files changed

+128
-25
lines changed

README.md

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,30 @@ techcable.orderedset
55
[![pypi](https://img.shields.io/pypi/v/techcable.orderedset)](https://pypi.org/project/techcable.orderedset/)
66
![types](https://img.shields.io/pypi/types/techcable.orderedset)]
77

8-
A simple and efficient pure-python ordered set.
8+
A simple and efficient `OrderedSet` in pure python. Implements both [`MutableSet`] and [`Sequence`].
9+
10+
[`MutableSet`]: https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableSet
11+
[`Sequence`]: https://docs.python.org/3/library/collections.abc.html#collections.abc.Sequence
12+
913

1014
## Example Usage
1115
```python
1216
from techcable.orderedset import OrderedSet
1317

1418
# prints {1, 2, 7, 3}
1519
print(OrderedSet([1, 2, 7, 2, 3]))
20+
# Implements all standard set methods, still preserves order
21+
print(OrderedSet([1,2]) | OrderedSet([3,2,4])) # {1,2,3,4}
22+
23+
24+
# Implements `append` method, returning True on success
25+
# and False if the item was a duplicate
26+
oset = OrderedSet()
27+
oset.append(1) # True
28+
oset.append(2) # True
29+
oset.append(1) # False - already in set, did nothing
30+
oset.extend([2,3]) # True - at least one success
31+
oset.append([2,3]) # False - all duplicates
1632
```
1733

1834
Supports [pydantic](pydantic.org) validation & serialization:
@@ -27,9 +43,9 @@ assert model.dump_python(OrderedSet([1,2,7,8])) == [1,2,7,8]
2743
```
2844

2945
## Potential Future Features
30-
- Add [acceleration module] using C/Rust/Cython
31-
- Implemented `OrderedFrozenSet`
32-
- Publish HTML documentation using Sphinx or [pdoc](https://pdoc.dev/)
46+
- Implement `OrderedFrozenSet`
47+
- Consider [acceleration module] using C/Rust/Cython
48+
- Probably unnecessary since this has library has very little overhead compared to the builtin `set`/`list`
3349

3450
[acceleration module]: https://peps.python.org/pep-0399/
3551

src/techcable/orderedset/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
"""
2+
Implements `OrderedSet`, a [`MutableSet`] that preserves insertion order and is also a [`Sequence`].
3+
4+
[`MutableSet`]: https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableSet
5+
[`Sequence`]: https://docs.python.org/3/library/collections.abc.html#collections.abc.Sequence
6+
7+
The implementation is pure-python and does not require any native code.
8+
"""
9+
110
from ._orderedset import OrderedSet
211
from ._version import __version__
312

src/techcable/orderedset/_orderedset.py

Lines changed: 99 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,36 @@ def override(v):
5454

5555

5656
class OrderedSet(MutableSet[T], Sequence[T]):
57-
"""A Set of elements, which preserves insertion order.
57+
"""
58+
A [`MutableSet`] that preserves insertion order and is also a [`Sequence`].
5859
59-
This type cannot implement `MutableSequence` because OrderedSet.append
60-
ignores duplicate elements and returns a `bool` instead of `None`."""
60+
[`MutableSet`]: https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableSet
61+
[`Sequence`]: https://docs.python.org/3/library/collections.abc.html#collections.abc.Sequence
62+
63+
## Conveniences
64+
Calling `OrderedSet.append` returns `True` if the element was successfully added,
65+
and `False` if the element is a duplicate.
66+
67+
Calling `str(OrderedSet([x, y]))` is equivalent to `f"{x!r, y!r}"`.
68+
This is much prettier than using `repr`,
69+
which is expected to roundtrip through `eval`.
70+
71+
## Gotchas
72+
This type does not implement [`MutableSequence`]
73+
because `OrderedSet.append` ignores duplicate elements
74+
and returns `bool` instead of `None`.
75+
76+
[`MutableSequence`]: https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableSequence
77+
78+
Pickling is currently not supported.
79+
80+
### Thread Safety
81+
This type is *NOT* thread safe.
82+
Concurrent mutation will result in unexpected behavior unless you use a lock.
83+
84+
Concurrent reads are fully supported,
85+
as long as no modifications are being made.
86+
"""
6187

6288
__slots__ = ("_unique", "_elements")
6389

@@ -66,7 +92,12 @@ class OrderedSet(MutableSet[T], Sequence[T]):
6692

6793
@override
6894
def __init__(self, source: Iterable[T] | None = None, /) -> None:
69-
"""Create an ordered set containing the specified elements"""
95+
"""
96+
Create an `OrderedSet` containing the specified elements.
97+
98+
This preserves the order of the original input
99+
and implicitly ignores duplicates.
100+
"""
70101
self._unique = set()
71102
self._elements = []
72103
if source is None:
@@ -83,10 +114,15 @@ def __init__(self, source: Iterable[T] | None = None, /) -> None:
83114
assert len(self._unique) == len(self._elements)
84115

85116
def append(self, value: T, /) -> bool:
86-
"""Append a value to the set if it doesn't already exist.
117+
"""
118+
Append a value to the set if it doesn't already exist.
87119
88120
Returns `True` if successfully added, or `False` if already exists.
89-
Note that the return value doesn't match list.append, which always returns `None`"""
121+
122+
There are two important differences between this method and `list.append`:
123+
1. This method does nothing if the value is a duplicate
124+
2. This method returns a `bool` instead of `None`
125+
"""
90126
is_new = value not in self._unique
91127
if is_new:
92128
self._unique.add(value)
@@ -98,7 +134,7 @@ def extend(self, values: Iterable[T], /) -> bool:
98134
"""
99135
Add all the specified values to the set.
100136
101-
Returns True if at least one element was added,
137+
Returns `True` if at least one element was added,
102138
or `False` if every element is a duplicate.
103139
104140
Roughly equivalent to `any(oset.append(v) for v in values)`.
@@ -110,26 +146,47 @@ def extend(self, values: Iterable[T], /) -> bool:
110146

111147
@override
112148
def add(self, value: T, /) -> None:
113-
"""Add a value to the set if it doesn't already exist.
149+
"""
150+
Add a value to the set if it doesn't already exist.
114151
115152
Return value is `None` for consistency with `set.add`.
116-
Use `OrderedSet.append` if you want to know if the element already existed."""
153+
Use `OrderedSet.append` if you want to know if the element already existed.
154+
"""
117155
self.append(value)
118156

119157
@override
120158
def discard(self, value: T, /) -> None:
121-
"""Remove the element from the set if it exists."""
159+
"""
160+
Remove an element from the set if it exists.
161+
162+
Unlike `OrderedSet.remove`, this method does not raise
163+
an exception if this element is missing.
164+
"""
122165
if value in self._unique:
123166
self._elements.remove(value)
124167
self._unique.remove(value)
125168

126169
def update(self, values: Iterable[T], /) -> None:
127-
"""Add all the"""
170+
"""
171+
Add all the specified values to this set.
172+
173+
Equivalent to running
174+
```
175+
for val in values:
176+
oset.append(val)
177+
```
178+
"""
128179
self.extend(values)
129180

130181
@override
131182
def pop(self, index: int = -1) -> T:
132-
"""Pop an item from the end of the list (or at `index`)"""
183+
"""
184+
Remove and return an item from the end of the list (or from `self[index]`).
185+
186+
Raises `IndexError` if the list is empty or `index` is out of bounds.
187+
188+
Equivalent to `list.pop`.
189+
"""
133190
item = self._elements.pop(index)
134191
self._unique.remove(item)
135192
return item
@@ -209,15 +266,19 @@ def __ge__(self, other: object) -> bool:
209266
def sort(
210267
self, key: Optional[Callable[[T], U]] = None, reverse: bool = False
211268
) -> None:
212-
"""Sort the elements in the set, as if calling list.sort"""
269+
"""Sort the elements of the set in-place, as if calling `list.sort`."""
213270
self._elements.sort(key=key, reverse=reverse)
214271

215272
def reverse(self) -> None:
216-
"""Reverse the elements in the set, as if calling list.reverse"""
273+
"""Reverse the elements of the set in-place, as if calling `list.reverse`."""
217274
self._elements.reverse()
218275

219276
def copy(self) -> OrderedSet[T]:
220-
"""Create a copy of the set"""
277+
"""
278+
Create a shallow copy of the set.
279+
280+
Equivalent to `OrderedSet(self)`
281+
"""
221282
return OrderedSet(self)
222283

223284
@override
@@ -261,12 +322,20 @@ def __get_pydantic_core_schema__(
261322

262323
@classmethod
263324
def dedup(self, source: Iterable[T], /) -> Generator[T]:
264-
"""A utility method to deduplicate the specified iterable,
265-
while preserving the original order.
325+
"""
326+
Yield unique elements, preserving order.
266327
267-
This is a generator, so does not need to wait for the entire input,
328+
This is a an iterator combinator (generator) similar to those in [`itertools`].
329+
It does not need to wait for the entire input,
268330
and will return items as soon as they are available.
269331
332+
[`itertools`]: https://docs.python.org/3/library/itertools.html
333+
334+
This is similar to [`more_itertools.unique_everseen`],
335+
although it uses an `OrderedSet` internally and does not support the `key` argument.
336+
337+
[`more_itertools.unique_everseen`]: https://more-itertools.readthedocs.io/en/v10.7.0/api.html#more_itertools.unique_everseen
338+
270339
Since: v0.1.4
271340
"""
272341
oset: OrderedSet[T] = OrderedSet()
@@ -277,14 +346,23 @@ def dedup(self, source: Iterable[T], /) -> Generator[T]:
277346

278347
@classmethod
279348
async def dedup_async(self, source: AsyncIterable[T], /) -> AsyncGenerator[T]:
280-
"""A utility method to deduplicate the specified iterable,
281-
while preserving the original order.
349+
"""
350+
Yield unique elements, preserving order.
282351
283-
This is a generator, so does not need to wait for the entire input.
352+
This is a an iterator combinator (generator) similar to those in [`itertools`].
353+
It does not need to wait for the entire input,
354+
and will return items as soon as they are available.
284355
Because it is asynchronous, it does not block the thread while waiting.
285356
286357
This is an asynchronous version of `OrderedSet.dedup`.
287358
359+
It is similar to [`more_itertools.unique_everseen`],
360+
but is asynchronous, uses an `OrderedSet` internally,
361+
and does not support the `key` argument.
362+
363+
[`itertools`]: https://docs.python.org/3/library/itertools.html
364+
[`more_itertools.unique_everseen`]: https://more-itertools.readthedocs.io/en/v10.7.0/api.html#more_itertools.unique_everseen
365+
288366
Since: v0.1.4
289367
"""
290368
# Defined in PEP 525

0 commit comments

Comments
 (0)