Skip to content

Commit 50ce2a0

Browse files
bhimrazyCopilot
andauthored
chore: add pyright type checking and ci checks for it (#280)
* add config to pyproject toml * add checks for pyright * update * update * fix: add type ignore comments for Redis storage methods and update type hints in MinHash and LSH classes * fix: update input validation in WeightedMinHashGenerator to check for sized iterables * fix: add type hints and improve error handling in storage functions * fix: add assertions to ensure keys and hashtables are initialized in AsyncMinHashLSH * fix: update _merge method signature to return None and improve error handling in _hashed_byteswap * fix: remove unnecessary assertion for upper bound in MinHashLSHEnsemble * fix: simplify hashvalues initialization in WeightedMinHashGenerator * fix: remove unnecessary assertions for keys and hashtables in AsyncMinHashLSH * refactor: remove duplicate parsing of hashvalues * refactor: update type hints for hashvalues and permutations in MinHash class * apply suggestion Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * revert seeds none change --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 4e29f97 commit 50ce2a0

9 files changed

Lines changed: 96 additions & 33 deletions

File tree

.github/workflows/checks.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,21 @@ jobs:
1010
- uses: actions/checkout@v6
1111
- uses: astral-sh/ruff-action@v3
1212

13+
pyright:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v6
17+
- uses: astral-sh/setup-uv@v7
18+
with:
19+
activate-environment: true
20+
python-version: "3.10"
21+
enable-cache: true
22+
- name: Test Pyright
23+
run: |
24+
uvx pyright
25+
- name: Minimize uv cache
26+
run: uv cache prune --ci
27+
1328
link-check:
1429
runs-on: ubuntu-latest
1530
steps:

datasketch/experimental/aio/storage.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -332,10 +332,10 @@ def initialized(self):
332332

333333
class AsyncRedisListStorage(OrderedStorage, AsyncRedisStorage):
334334
async def keys(self):
335-
return await self._redis.hkeys(self._name)
335+
return await self._redis.hkeys(self._name) # type: ignore
336336

337337
async def redis_keys(self):
338-
return await self._redis.hvals(self._name)
338+
return await self._redis.hvals(self._name) # type: ignore
339339

340340
def status(self):
341341
status = self._parse_config(self.config["redis"])
@@ -374,8 +374,8 @@ async def remove_val(self, key, val, **kwargs):
374374
await self._buffer.lrem(redis_key, val)
375375
else:
376376
await self._redis.lrem(redis_key, val)
377-
if not await self._redis.exists(redis_key):
378-
await self._redis.hdel(self._name, redis_key)
377+
if not await self._redis.exists(redis_key): # type: ignore
378+
await self._redis.hdel(self._name, redis_key) # type: ignore
379379

380380
async def insert(self, key, *vals, **kwargs):
381381
# Using buffer=True outside of an `insertion_session`
@@ -394,7 +394,7 @@ async def _insert(self, r, key, *values):
394394
await r.rpush(redis_key, *values)
395395

396396
async def size(self):
397-
return await self._redis.hlen(self._name)
397+
return await self._redis.hlen(self._name) # type: ignore
398398

399399
async def itemcounts(self):
400400
pipe = self._redis.pipeline()
@@ -409,7 +409,7 @@ async def _get_len(r, k):
409409
return await r.llen(k)
410410

411411
async def has_key(self, key):
412-
return await self._redis.hexists(self._name, key)
412+
return await self._redis.hexists(self._name, key) # type: ignore
413413

414414
async def empty_buffer(self):
415415
await self._buffer.execute()
@@ -429,8 +429,8 @@ async def remove_val(self, key, val, **kwargs):
429429
await self._buffer.srem(redis_key, val)
430430
else:
431431
await self._redis.srem(redis_key, val)
432-
if not await self._redis.exists(redis_key):
433-
await self._redis.hdel(self._name, redis_key)
432+
if not await self._redis.exists(redis_key): # type: ignore
433+
await self._redis.hdel(self._name, redis_key) # type: ignore
434434

435435
async def _insert(self, r, key, *values):
436436
redis_key = self.redis_key(key)

datasketch/lsh.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,18 @@
33
import pickle
44
import struct
55
from collections.abc import Hashable
6-
from typing import Callable, Optional, Union
6+
from typing import Callable, List, Optional, Union
77

88
from scipy.integrate import quad as integrate
99

1010
from datasketch.minhash import MinHash
11-
from datasketch.storage import _random_name, ordered_storage, unordered_storage
11+
from datasketch.storage import (
12+
OrderedStorage,
13+
UnorderedStorage,
14+
_random_name,
15+
ordered_storage,
16+
unordered_storage,
17+
)
1218
from datasketch.weighted_minhash import WeightedMinHash
1319

1420

@@ -183,15 +189,15 @@ def __init__(
183189
self._H = self._byteswap
184190

185191
basename = storage_config.get("basename", _random_name(11))
186-
self.hashtables = [
192+
self.hashtables: List[UnorderedStorage] = [
187193
unordered_storage(
188194
storage_config,
189195
name=b"".join([basename, b"_bucket_", struct.pack(">H", i)]),
190196
)
191197
for i in range(self.b)
192198
]
193199
self.hashranges = [(i * self.r, (i + 1) * self.r) for i in range(self.b)]
194-
self.keys = ordered_storage(storage_config, name=b"".join([basename, b"_keys"]))
200+
self.keys: OrderedStorage = ordered_storage(storage_config, name=b"".join([basename, b"_keys"]))
195201

196202
@property
197203
def buffer_size(self) -> int:
@@ -347,7 +353,7 @@ def __equivalent(self, other: MinHashLSH) -> bool:
347353
"""
348354
return type(self) is type(other) and self.h == other.h and self.b == other.b and self.r == other.r
349355

350-
def _merge(self, other: MinHashLSH, check_overlap: bool = False, buffer: bool = False) -> MinHashLSH:
356+
def _merge(self, other: MinHashLSH, check_overlap: bool = False, buffer: bool = False) -> None:
351357
if self.__equivalent(other):
352358
if check_overlap and set(self.keys).intersection(set(other.keys)):
353359
raise ValueError("The keys are overlapping, duplicate key exists.")
@@ -524,6 +530,8 @@ def _byteswap(self, hs):
524530
return bytes(hs.byteswap().data)
525531

526532
def _hashed_byteswap(self, hs):
533+
if self.hashfunc is None:
534+
raise RuntimeError("Hash function not configured.")
527535
return self.hashfunc(bytes(hs.byteswap().data))
528536

529537
def _query_b(self, minhash, b):

datasketch/lsh_bloom.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,9 +252,9 @@ def __init__(
252252
raise ValueError("threshold must be in [0.0, 1.0]")
253253
if num_perm < 2:
254254
raise ValueError("Too few permutation functions")
255-
if n <= 0:
255+
if n is None or n <= 0:
256256
raise ValueError("n for LSHBloom must be >= 0")
257-
if fp >= 1.0 or fp <= 0.0:
257+
if fp is None or fp >= 1.0 or fp <= 0.0:
258258
raise ValueError("fp must be in (0.0, 1.0)")
259259
if save_dir is None:
260260
warnings.warn(

datasketch/lshensemble.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,8 @@ def index(self, entries: Iterable[tuple[Hashable, MinHash, int]]) -> None:
221221
entries.sort(key=lambda e: e[2])
222222
curr_part = 0
223223
for key, minhash, size in entries:
224-
if size > self.uppers[curr_part]:
224+
u = self.uppers[curr_part]
225+
if size > u:
225226
curr_part += 1
226227
for r in self.indexes[curr_part]:
227228
self.indexes[curr_part][r].insert(key, minhash)

datasketch/minhash.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@
33
import copy
44
import warnings
55
from collections.abc import Generator, Iterable
6-
from typing import Callable, Optional
6+
from typing import TYPE_CHECKING, Callable, Optional, Union
77

88
try:
99
from typing import Literal # py3.8+; if older, you can fallback to typing_extensions
10-
except Exception:
10+
except ImportError:
1111
from typing_extensions import Literal
1212

1313
import numpy as np
1414

15+
if TYPE_CHECKING:
16+
from numpy.typing import ArrayLike
17+
1518
# GPU backend
1619
try:
1720
import cupy as cp
@@ -114,8 +117,8 @@ def __init__(
114117
gpu_mode: Literal["disable", "detect", "always"] = "disable",
115118
hashfunc: Callable = sha1_hash32,
116119
hashobj: Optional[object] = None, # Deprecated.
117-
hashvalues: Optional[Iterable] = None,
118-
permutations: Optional[tuple[Iterable, Iterable]] = None,
120+
hashvalues: Optional[ArrayLike] = None,
121+
permutations: Optional[Union[tuple[ArrayLike, ArrayLike], ArrayLike]] = None,
119122
) -> None:
120123
if hashvalues is not None:
121124
num_perm = len(hashvalues)
@@ -180,7 +183,7 @@ def _init_permutations(self, num_perm: int) -> np.ndarray:
180183
dtype=np.uint64,
181184
).T
182185

183-
def _parse_hashvalues(self, hashvalues):
186+
def _parse_hashvalues(self, hashvalues) -> np.ndarray:
184187
return np.array(hashvalues, dtype=np.uint64)
185188

186189
def update(self, b) -> None:

datasketch/storage.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
c_concurrent = None
2727

2828

29-
def ordered_storage(config, name=None):
29+
def ordered_storage(config, name=None) -> "OrderedStorage":
3030
"""Return ordered storage system based on the specified config.
3131
3232
The canonical example of such a storage container is
@@ -62,10 +62,10 @@ def ordered_storage(config, name=None):
6262
return RedisListStorage(config, name=name)
6363
if tp == "cassandra":
6464
return CassandraListStorage(config, name=name)
65-
return None
65+
raise ValueError(f"Unknown storage type: {tp}")
6666

6767

68-
def unordered_storage(config, name=None):
68+
def unordered_storage(config, name=None) -> "UnorderedStorage":
6969
"""Return an unordered storage system based on the specified config.
7070
7171
The canonical example of such a storage container is
@@ -100,7 +100,7 @@ def unordered_storage(config, name=None):
100100
return RedisSetStorage(config, name=name)
101101
if tp == "cassandra":
102102
return CassandraSetStorage(config, name=name)
103-
return None
103+
raise ValueError(f"Unknown storage type: {tp}")
104104

105105

106106
class Storage(ABC):
@@ -144,7 +144,7 @@ def insert(self, key, *vals, **kwargs):
144144
pass
145145

146146
@abstractmethod
147-
def remove(self, *keys):
147+
def remove(self, *keys, **kwargs):
148148
"""Remove `keys` from storage."""
149149
pass
150150

@@ -154,12 +154,12 @@ def remove_val(self, key, val):
154154
pass
155155

156156
@abstractmethod
157-
def size(self):
157+
def size(self) -> int:
158158
"""Return size of storage with respect to number of keys."""
159159
pass
160160

161161
@abstractmethod
162-
def itemcounts(self, **kwargs):
162+
def itemcounts(self, **kwargs) -> dict:
163163
"""Returns the number of items stored under each key."""
164164
pass
165165

@@ -168,6 +168,14 @@ def has_key(self, key):
168168
"""Determines whether the key is in the storage or not."""
169169
pass
170170

171+
@property
172+
def buffer_size(self) -> int:
173+
return getattr(self, "_buffer_size", 50000)
174+
175+
@buffer_size.setter
176+
def buffer_size(self, value: int):
177+
self._buffer_size = value
178+
171179
def status(self):
172180
return {"keyspace_size": len(self)}
173181

datasketch/weighted_minhash.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -133,14 +133,15 @@ def minhash(self, v: np.ndarray) -> WeightedMinHash:
133133
WeightedMinHash: The weighted MinHash.
134134
135135
"""
136-
if not isinstance(v, collections.abc.Iterable):
137-
raise TypeError("Input vector must be an iterable")
136+
if not isinstance(v, collections.abc.Sized):
137+
raise TypeError("Input vector must be sized")
138138
if not len(v) == self.dim:
139139
raise ValueError("Input dimension mismatch, expecting %d" % self.dim)
140140
if not isinstance(v, np.ndarray):
141141
v = np.array(v, dtype=np.float32)
142142
elif v.dtype != np.float32:
143143
v = v.astype(np.float32)
144+
v: np.ndarray = v
144145
hashvalues = np.zeros((self.sample_size, 2), dtype=int)
145146
vzeros = v == 0
146147
if vzeros.all():
@@ -226,9 +227,8 @@ def minhash_many(self, X: Union[sp.sparse.spmatrix, np.ndarray]) -> list[Union[W
226227
doc_argmin = np.argmin(doc_ln_a, axis=1)
227228
doc_k = doc_cidx[doc_argmin]
228229

229-
all_hashvalues[it_doc] = np.zeros((self.sample_size, 2), dtype=int)
230-
231-
hashvalues = all_hashvalues[it_doc]
230+
hashvalues = np.zeros((self.sample_size, 2), dtype=int)
231+
all_hashvalues[it_doc] = hashvalues
232232
hashvalues[:, 0], hashvalues[:, 1] = (
233233
doc_k,
234234
t[np.arange(self.sample_size), doc_begin + doc_argmin],

pyproject.toml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,34 @@ addopts = ["--strict-markers", "--color=yes", "--cov-report=xml"]
164164
testpaths = ["test"]
165165
asyncio_mode = "auto"
166166

167+
[tool.pyright]
168+
include = ["datasketch"]
169+
exclude = [
170+
"benchmark",
171+
"docs",
172+
"examples",
173+
"test",
174+
"travis",
175+
"**/.venv/**",
176+
"**/__pycache__",
177+
]
178+
pythonVersion = "3.9"
179+
typeCheckingMode = "basic" # todo: change to "strict" in future
180+
181+
reportMissingImports = "none"
182+
reportUnusedVariable = "warning"
183+
reportAttributeAccessIssue = "none"
184+
reportOptionalMemberAccess = "none"
185+
reportGeneralTypeIssues = "none"
186+
reportArgumentType = "none"
187+
reportOptionalIterable = "none"
188+
reportReturnType = "none"
189+
reportRedeclaration = "none"
190+
reportOperatorIssue = "none"
191+
reportAssignmentType = "none"
192+
reportOptionalSubscript = "none"
193+
reportCallIssue = "none"
194+
167195
[tool.coverage.run]
168196
source = ["datasketch"]
169197
omit = ["*/experimental/*", "*/tests/*", "*/test/*"]

0 commit comments

Comments
 (0)