fix(probes): download wordnet lexicon when it is missing from the database#1820
fix(probes): download wordnet lexicon when it is missing from the database#1820adityasingh2400 wants to merge 2 commits into
Conversation
The Wordnet topic probes set wn's data directory to a garak-specific cache location. On the first run that cache holds no lexicon, so the constructor catches sqlite3.OperationalError and downloads the lexicon. When the database file already exists but the requested lexicon row is not installed, wn raises wn.Error rather than sqlite3.OperationalError. That case was not caught, so the probe failed to load with "no lexicon found with lang=None and lexicon='oewn:2023'", which is exactly what users hit in issue NVIDIA#1230. This widens the except clause to also catch wn.Error so the same download-and-retry recovery runs in both situations. The fix matches the wn maintainer's diagnosis on the issue that the lexicon was simply absent from the database and a reinstall does not rebuild it. Adds a parametrized test that mocks wn.Wordnet to raise each error on first load and asserts the probe downloads the lexicon and keeps the reloaded handle. The wn.Error case fails before this change and passes after it. Fixes NVIDIA#1230 Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Aditya Singh <adisin650@gmail.com>
leondz
left a comment
There was a problem hiding this comment.
generally in the right place - minor queries
| sqlite3.OperationalError("no such table: lexicons"), | ||
| wn.Error("no lexicon found with lang=None and lexicon='oewn:2023'"), |
There was a problem hiding this comment.
String-matched exceptions are brittle. Is this the best way of catching these? What if wn version becomes unpinned?
| import sqlite3 | ||
| from unittest.mock import MagicMock, patch | ||
|
|
||
| import pytest |
There was a problem hiding this comment.
imports on L4-L8 should be in a single sorted block, with wn on its own
| patch.object(wn, "download", return_value=download_path) as mock_download, | ||
| patch.object(pathlib.Path, "rmdir"), | ||
| ): | ||
| probe = garak.probes.topic.WordnetBlockedWords() |
There was a problem hiding this comment.
This is an unconventional instantiation path; why not use garak._plugins.load_plugin()?
|
Thanks for the review. Addressed all three in c46fb60:
Let me know if you would prefer the test exercised a different way. |
- Group the new imports into a single sorted block and keep wn separate - Use generic exception messages since the recovery path matches on the exception type, not the message text, so an unpinned wn version will not break the test - Load the probe through garak._plugins.load_plugin like the other tests instead of instantiating the class directly Signed-off-by: Aditya Singh <adisin650@gmail.com>
c46fb60 to
b8e8335
Compare
Fixes #1230
Root cause
The Wordnet topic probes point
wnat a garak-specific cache directory (garak/probes/topic.py:104). On a clean cache the SQLite database does not exist yet, sown.Wordnet(self.lexicon)raisessqlite3.OperationalError, which the constructor catches to trigger a one-off lexicon download and retry.There is a second failure mode the constructor did not handle. When the database file already exists but the requested lexicon row has never been installed,
wnraiseswn.Errorinstead, for exampleno lexicon found with lang=None and lexicon=oewn:2023. That exception was not caught, so the probe failed to load and the run ended early. This is exactly the traceback reported in the issue. Thewnmaintainer confirmed on the issue thread that the underlying cause is the lexicon being absent from the database, and that reinstalling the package does not rebuild it. Since garak uses a custom data directory, the lexicon is frequently not present, so this path is easy to hit.Fix
Widen the
exceptclause inWordnetBlockedWords.__init__to also catchwn.Error, so the same download-and-retry recovery runs whether the database is missing entirely or merely lacks the requested lexicon. A short comment documents which condition each exception represents. This keeps the change to a single, specific exception type as the contribution guide asks, and avoids a broadexcept.Why this is not a duplicate
There is no open or closed PR addressing issue #1230. I checked with
gh pr list --repo NVIDIA/garak --state all --search "1230 in:body",gh pr list --search "topic wordnet", andgh pr list --search "wordnet lexicon". The two prior cross-references (#1316 and #764) are unrelated merged changes to howwnis used and to the topic probe content, neither of which catcheswn.Error.Verification
Reproduced the exact exception against the pinned
wn==0.9.5by pointingwnat an empty data directory and callingwn.Wordnet("oewn:2023"), which raiseswn.Errorrather thansqlite3.OperationalError, confirming the gap.Added a parametrized regression test that mocks
wn.Wordnetto raise each error on first load and asserts the probe downloads the lexicon once, reloads it, and keeps the reloaded handle. Thewn.Errorcase fails on current main and passes with this change. Thesqlite3.OperationalErrorcase passes in both, guarding the existing behaviour.Commands run, on Python 3.13 with the test extras installed:
Disclosure
AI assistance was used to help investigate and draft this change. I reviewed every changed line, confirmed the root cause against the pinned
wnversion, and ran the tests and linters above.