Skip to content

Commit b43a0ac

Browse files
committed
Address review: clean error messages and validate stored hash segments
Collapse backslash line-continuations inside ValueError message literals (13 sites in minhash.py, lean_minhash.py, b_bit_minhash.py) -- they embedded the source indentation into user-facing error text. Validate segment bytes in MinHashLSHForest.get_minhash_hashvalues before inferring the value width: the segment length must be a whole number of 4- or 8-byte values and all segments must agree, so corrupted or truncated storage raises ValueError instead of being silently reinterpreted with the wrong dtype. Add a corrupt-segment test.
1 parent 780bb71 commit b43a0ac

5 files changed

Lines changed: 41 additions & 29 deletions

File tree

datasketch/b_bit_minhash.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,18 +83,15 @@ def jaccard(self, other):
8383
"""
8484
if self.b != other.b:
8585
raise ValueError(
86-
"Cannot compare two b-bit MinHashes with different\
87-
b values"
86+
"Cannot compare two b-bit MinHashes with different b values"
8887
)
8988
if self.scheme != other.scheme:
9089
raise ValueError(
91-
"Cannot compare two b-bit MinHashes with different\
92-
permutation schemes"
90+
"Cannot compare two b-bit MinHashes with different permutation schemes"
9391
)
9492
if self.seed != other.seed:
9593
raise ValueError(
96-
"Cannot compare two b-bit MinHashes with different\
97-
set of permutations"
94+
"Cannot compare two b-bit MinHashes with different set of permutations"
9895
)
9996
intersection = np.count_nonzero(self.hashvalues == other.hashvalues)
10097
raw_est = float(intersection) / float(self.hashvalues.size)

datasketch/lean_minhash.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -248,8 +248,7 @@ def serialize(self, buf, byteorder="@") -> None:
248248
"""
249249
if len(buf) < self.bytesize(byteorder):
250250
raise ValueError(
251-
"The buffer does not have enough space\
252-
for holding this MinHash."
251+
"The buffer does not have enough space for holding this MinHash."
253252
)
254253
if self.scheme == _SCHEME_LEGACY:
255254
fmt = "%sqi%dI" % (byteorder, len(self))
@@ -351,8 +350,7 @@ def union(cls, *lmhs: LeanMinHash) -> LeanMinHash:
351350
scheme = lmhs[0].scheme
352351
if any((seed != m.seed or num_perm != len(m) or scheme != m.scheme) for m in lmhs):
353352
raise ValueError(
354-
"The unioning MinHash must have the\
355-
same seed, number of permutation functions and scheme."
353+
"The unioning MinHash must have the same seed, number of permutation functions and scheme."
356354
)
357355
hashvalues = np.minimum.reduce([m.hashvalues for m in lmhs])
358356

datasketch/lshforest.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,10 +150,20 @@ def get_minhash_hashvalues(self, key: Hashable) -> np.ndarray:
150150
f"The provided key does not exist in the LSHForest: {key}")
151151
# Each stored segment packs self.k hash values; derive the value byte
152152
# width from the segment size (4 for the "affine32" MinHash scheme,
153-
# 8 for "affine64" and "legacy").
154-
bytes_per_value = len(byteslist[0]) // self.k
153+
# 8 for "affine64" and "legacy"). Validate before trusting it, so
154+
# corrupted or truncated storage raises instead of being silently
155+
# reinterpreted with the wrong width.
156+
segment_size = len(byteslist[0])
157+
bytes_per_value, remainder = divmod(segment_size, self.k)
158+
if remainder != 0 or bytes_per_value not in (4, 8):
159+
raise ValueError(
160+
"Stored hash segment of %d bytes is not %d values of a "
161+
"supported width (4 or 8 bytes)" % (segment_size, self.k)
162+
)
163+
if any(len(item) != segment_size for item in byteslist):
164+
raise ValueError("Stored hash segments have inconsistent sizes")
155165
dtype = np.uint32 if bytes_per_value == 4 else np.uint64
156-
values_per_segment = len(byteslist[0]) // np.dtype(dtype).itemsize
166+
values_per_segment = segment_size // np.dtype(dtype).itemsize
157167
hashvalues = np.empty(
158168
len(byteslist) * values_per_segment, dtype=dtype)
159169
for index, item in enumerate(byteslist):

datasketch/minhash.py

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -279,8 +279,7 @@ def __init__(
279279
# Because 1) we don't want the size to be too large, and
280280
# 2) we are using 4 bytes to store the size value
281281
raise ValueError(
282-
"Cannot have more than %d number of\
283-
permutation functions"
282+
"Cannot have more than %d number of permutation functions"
284283
% _hash_range
285284
)
286285
self.seed = seed
@@ -556,18 +555,15 @@ def jaccard(self, other: MinHash) -> float:
556555
"""
557556
if getattr(other, "scheme", None) != self.scheme:
558557
raise ValueError(
559-
"Cannot compute Jaccard given MinHash with\
560-
different permutation schemes"
558+
"Cannot compute Jaccard given MinHash with different permutation schemes"
561559
)
562560
if other.seed != self.seed:
563561
raise ValueError(
564-
"Cannot compute Jaccard given MinHash with\
565-
different seeds"
562+
"Cannot compute Jaccard given MinHash with different seeds"
566563
)
567564
if len(self) != len(other):
568565
raise ValueError(
569-
"Cannot compute Jaccard given MinHash with\
570-
different numbers of permutation functions"
566+
"Cannot compute Jaccard given MinHash with different numbers of permutation functions"
571567
)
572568
return float(np.count_nonzero(self.hashvalues == other.hashvalues)) / float(len(self))
573569

@@ -597,18 +593,15 @@ def merge(self, other: MinHash) -> None:
597593
"""
598594
if getattr(other, "scheme", None) != self.scheme:
599595
raise ValueError(
600-
"Cannot merge MinHash with\
601-
different permutation schemes"
596+
"Cannot merge MinHash with different permutation schemes"
602597
)
603598
if other.seed != self.seed:
604599
raise ValueError(
605-
"Cannot merge MinHash with\
606-
different seeds"
600+
"Cannot merge MinHash with different seeds"
607601
)
608602
if len(self) != len(other):
609603
raise ValueError(
610-
"Cannot merge MinHash with\
611-
different numbers of permutation functions"
604+
"Cannot merge MinHash with different numbers of permutation functions"
612605
)
613606
self.hashvalues = np.minimum(other.hashvalues, self.hashvalues)
614607

@@ -707,8 +700,7 @@ def union(cls, *mhs: MinHash) -> MinHash:
707700
scheme = mhs[0].scheme
708701
if any((seed != m.seed or num_perm != len(m) or scheme != m.scheme) for m in mhs):
709702
raise ValueError(
710-
"The unioning MinHash must have the\
711-
same seed, number of permutation functions and scheme"
703+
"The unioning MinHash must have the same seed, number of permutation functions and scheme"
712704
)
713705
hashvalues = np.minimum.reduce([m.hashvalues for m in mhs])
714706
permutations = mhs[0].permutations

test/test_lshforest.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,21 @@ def test_get_minhash_hashvalues(self):
7575
for i in range(len(retrieved_hashvalues)):
7676
self.assertEqual(hashvalues[i], retrieved_hashvalues[i])
7777

78+
def test_get_minhash_hashvalues_rejects_corrupt_segments(self):
79+
forest, data = self._setup()
80+
key = next(iter(data))
81+
segments = forest.keys[key]
82+
# Truncated first segment: no longer a whole number of hash values
83+
# of a supported width, must not be silently reinterpreted.
84+
forest.keys[key] = [segments[0][:-3], *segments[1:]]
85+
with self.assertRaises(ValueError):
86+
forest.get_minhash_hashvalues(key)
87+
# First segment intact but a later one truncated: inconsistent sizes.
88+
forest.keys[key] = [*segments[:-1], segments[-1][: len(segments[-1]) // 2]]
89+
with self.assertRaises(ValueError):
90+
forest.get_minhash_hashvalues(key)
91+
forest.keys[key] = segments
92+
7893
def test_pickle(self):
7994
forest, _ = self._setup()
8095
forest2 = pickle.loads(pickle.dumps(forest))

0 commit comments

Comments
 (0)