fix(analyzer): make every recognizer listed in default_recognizers.yaml loadable - #2170
Conversation
7f986a9 to
6b50956
Compare
6b50956 to
2a3917e
Compare
…nguage KrPassportRecognizer was the only Korean recognizer absent from default_recognizers.yaml, for two reasons that also kept it out of the predefined registry. RecognizerListLoader.get instantiates every predefined recognizer with a name keyword argument, taken from the YAML entry (or from name when class_name supplies the class). KrPassportRecognizer.__init__ did not accept it, so listing the recognizer in default_recognizers.yaml made the registry raise TypeError on load. Adding the argument is what makes the entry possible. Its default supported_language was kr, while every other Korean recognizer defaults to ko, the ISO 639-1 code. data-privacy-stack#1742 migrated the Korean recognizers from kr to ko; data-privacy-stack#1814 added this one afterwards and reintroduced kr, so an AnalyzerEngine running ko silently skipped it. The default is now ko. This only affects direct instantiation: when an entry omits supported_languages the loader passes the registry's language explicitly, so the class default never applies on the YAML path. Registers the recognizer with enabled: false and country_code: kr, matching its siblings, and covers all three points with tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…y YAML
Three recognizers ship in default_recognizers.yaml with enabled: false but
cannot be turned on. KrBrnRecognizer, KrDriverLicenseRecognizer and
UsMbiRecognizer do not accept the name keyword argument that
RecognizerListLoader passes to every predefined recognizer, so flipping
enabled to true raises
TypeError: __init__() got an unexpected keyword argument 'name'
before the registry finishes loading. enabled: false is an opt-in switch,
not a disclaimer: an entry that cannot be enabled should not be listed.
The failure also reads as a user configuration error even though the YAML
is correct, unlike the optional-dependency entries, which refuse to load
with an actionable ImportError.
Adds the argument to the three constructors, and adds contract tests so
the next one is caught by CI.
Why this survived: the constructor signature is part of a contract that
nothing enforced. Each recognizer's own tests instantiate the class
directly, where no name is passed, so they all pass. The registry-level
tests build the default configuration, in which roughly 60 entries are
disabled and therefore never constructed. Nothing in between ever looked.
The new tests close that gap from both sides:
- Every predefined PatternRecognizer subclass must accept the kwargs the
loader passes. This fires when the class is added, before it reaches the
YAML at all, which is the point at which KrPassportRecognizer went
wrong in data-privacy-stack#1814.
- Every entry in default_recognizers.yaml must resolve to a class and must
load once enabled, exercised entry by entry so a failure names the
recognizer.
- A class_name plus name entry must produce an instance with the
configured name, which is the documented reason the loader passes name
and what makes the kwarg contract load-bearing.
Verified by reverting the three constructors: the signature test and the
load test each fail for exactly those three, with no other failures.
One entry is excluded from the load test by name, with its reason recorded
next to the exclusion, and a further test asserts each exclusion still
matches a shipped entry so the list cannot rot:
- HuggingFaceNerRecognizer cannot load from its shipped entry even with
its dependencies installed. EntityRecognizer.__init__ calls load()
unconditionally, load() requires model_name, and the entry does not
supply one, so it raises ValueError rather than the ImportError it
raises when transformers is absent. Supplying model_name here would make
the test download a model. The entry stays covered by the resolve test.
The load test skips only BasicLangExtractRecognizer, and only on
ImportError, because refusing to load without the langextract extra is that
recognizer's intended behavior. The skip is scoped to that name rather than
to the exception type, so an ImportError from any other entry is a failure
instead of a green skip.
BasicLangExtractRecognizer also carries a config_path that the recognizer
resolves against the working directory, so it raises FileNotFoundError when
pytest runs from the repository root. The load test sets the working
directory to the component root, the same one CI uses, instead of catching
that error: catching FileNotFoundError would turn a deleted or renamed
shipped config file into a passing skip. A separate test asserts every
config_path in the shipped configuration resolves to a file that exists,
which holds even in an environment that cannot construct the recognizer at
all.
The load test deliberately covers non-pattern entries too. Narrowing it to
PatternRecognizer subclasses would silently drop PhoneRecognizer,
ZaMobileNumberRecognizer and ZaTelephoneNumberRecognizer, which are not
PatternRecognizers and do load.
The signature test is scoped to PatternRecognizer subclasses.
AzureAILanguageRecognizer is the one remaining class that does not accept
name; it is absent from default_recognizers.yaml and fixes its own display
name, so it is out of scope here, but it is reachable from a user config by
class name and fails the same way. That is pre-existing and tracked
separately.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2a3917e to
ad36889
Compare
There was a problem hiding this comment.
🟡 Not ready to approve
The updated KR passport test adds avoidable debug output and uses tempfile.mkdtemp() without guaranteed cleanup, which should be corrected before merging.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Pull request overview
This PR strengthens the Presidio Analyzer predefined recognizer loading contract by ensuring YAML-configured recognizers are instantiable (notably when enabled: false entries are toggled on), and adds contract tests to prevent regressions.
Changes:
- Updated several predefined recognizers to accept the
namekwarg (as passed byRecognizerListLoader) and forward it tosuper().__init__. - Corrected
KrPassportRecognizer’s defaultsupported_languagetokoand registered it indefault_recognizers.yaml(disabled by default). - Added contract tests to validate (a) recognizer constructor signature compatibility and (b) that each shipped YAML entry resolves and loads when enabled.
File summaries
| File | Description |
|---|---|
| presidio-analyzer/tests/test_predefined_recognizer_contract.py | Adds contract tests for loader/recognizer constructor compatibility and for shipped YAML entry loadability. |
| presidio-analyzer/tests/test_kr_passport_recognizer.py | Extends KR passport tests to cover default language, name kwarg acceptance, and loading from the shipped YAML. |
| presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/us/us_mbi_recognizer.py | Makes UsMbiRecognizer loadable from YAML by accepting and forwarding name. |
| presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/korea/kr_passport_recognizer.py | Fixes default language (kr → ko) and adds name parameter forwarding for YAML instantiation. |
| presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/korea/kr_driver_license_recognizer.py | Accepts and forwards name to support YAML-driven instantiation. |
| presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/korea/kr_brn_recognizer.py | Accepts and forwards name to support YAML-driven instantiation. |
| presidio-analyzer/presidio_analyzer/conf/default_recognizers.yaml | Registers KrPassportRecognizer (disabled) with country/language metadata. |
Review details
- Files reviewed: 7/7 changed files
- Comments generated: 2
- Review effort level: Lite
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
Addresses the review on data-privacy-stack#2170. The KR passport YAML load test wrote the entry to a `tempfile.mkdtemp()` directory that nothing removed. Rather than wrap it in a context manager, the temporary file is gone: `RecognizerRegistryProvider` accepts the parsed mapping via `registry_configuration`, which is what the sibling contract test in this PR already does, so the round trip through the filesystem was never needed. Its assertion also only checked that `KR_PASSPORT` was among the loaded entities. Both tests now assert on the loaded recognizer *class*: the loader drops a recognizer whose language the registry does not support with a log warning and no exception, so a non-empty registry does not on its own prove the entry under test is what loaded. Also in this commit: - Removed a leftover debug `print` from the parametrized passport test. It predates this PR -- stripping trailing whitespace on the line above pulled it into the diff -- but it is adjacent to code this PR touches. - `default_recognizers.yaml` is parsed once at import instead of once per parametrized load test, which re-read it for the same two keys. - A `**kwargs` constructor now reports `pytest.skip` with a reason rather than returning as a silent pass, so the gap stays visible. No shipped `PatternRecognizer` subclass takes `**kwargs` today. - `test_yaml_entry_class_resolves` asserts the resolved object is an `EntityRecognizer` subclass instead of relying on the lookup raising. - Dropped the entry count from the module docstring. It was accurate but drifts every time a recognizer is added.
omri374
left a comment
There was a problem hiding this comment.
Thanks! Looks great, left a minor comment on tests.
Review feedback: tests in this repo are arranged by the file they test, so a standalone test_predefined_recognizer_contract.py is the wrong shape. Moved into tests/test_recognizers_loader_utils.py, which covers recognizers_loader_utils.py -- the module whose contract these assert. RecognizerListLoader is what passes `name`/`supported_language` to every predefined recognizer, what resolves an entry's class, and what consumes default_recognizers.yaml. That file already hosts the sibling shipped-YAML check (test_default_recognizers_yaml_country_code_matches_class), so the two now sit together instead of in separate files. The tests themselves are unchanged; the module docstring became a section header. test_default_recognizers_yaml_country_code_matches_class now reads the shipped YAML through the module-level constant the move introduced, rather than re-opening the same file a second time in one module. Test count is unchanged at 295 (294 pass, 1 skip -- BasicLangExtract, no optional dependency installed). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
I filed #2217 / #2218 for the same One small suggestion while I was in this area: a signature-level regression test keeps future yaml entries honest without instantiating anything (instantiation sweeps don't work here because ML entries like def test_when_default_yaml_lists_a_predefined_recognizer_then_its_constructor_accepts_name():
conf = yaml.safe_load(DEFAULT_RECOGNIZERS_YAML.read_text())
rejecting = []
for recognizer_conf in conf["recognizers"]:
if recognizer_conf.get("type") != "predefined":
continue
cls = getattr(predefined, recognizer_conf.get("class_name", recognizer_conf["name"]))
params = inspect.signature(cls.__init__).parameters
accepts_kwargs = any(p.kind is p.VAR_KEYWORD for p in params.values())
if "name" not in params and not accepts_kwargs:
rejecting.append(recognizer_conf["name"])
assert not rejectingBefore your fix it names exactly the four offenders. Feel free to take it or ignore it, happy either way. Thanks for fixing this. |
…izers too Follow-up to review feedback on data-privacy-stack#2170 suggesting a signature-level check driven from default_recognizers.yaml. The signature sweep was package-driven and scoped to PatternRecognizer subclasses, which left five shipped entries unreached: PhoneRecognizer, the two Za* ones, and the NER/LLM wrappers. Three of those the load test already constructs outright, but HuggingFaceNerRecognizer is excluded from it (its shipped entry supplies no model_name), so nothing in the suite touched its constructor at all. Parametrize over the union of both sources instead. Neither subsumes the other: the package sweep reaches a class before it is listed anywhere, which is how KrPassportRecognizer failed -- it could not be added to the yaml at all, so a yaml-driven check could never have named it -- while the yaml sweep reaches a listed class the package sweep skips by base class. Renamed to test_recognizer_accepts_loader_kwargs, since it is no longer pattern-only. Adds 5 params: 3 assertions and 2 skips, the skips being the **kwargs constructors whose signature cannot show which kwargs they honor. HuggingFaceNerRecognizer now surfaces as a named skip explaining why it is uncovered, rather than being silently absent. 297 passed, 3 skipped (was 294 / 1). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
No apology needed — and thanks for rescoping #2218 rather than dropping it. Took the suggestion, in Where you were right. My signature sweep was package-driven and scoped to So the test now parametrizes over the union of both sources. Neither subsumes the other, which is the part worth recording:
One correction, and it's the reason the sweep starts from the package. A yaml-driven check names three offenders on And one thing that didn't work out the way the snippet implies. Net effect: 5 new params — 3 assertions, 2 skips (
|
|
The 3-vs-4 mismatch is on me: I counted on my own branch, which already had the KrPassport yaml entry added, so the yaml-driven check saw four there. On The union parametrization plus the named skip is strictly better than what I posted. Thanks for measuring it instead of taking it as-is; the I'll file the HuggingFaceNerRecognizer issue. I hit the same edge from the other direction (an enable-everything sweep while benchmarking #2216) and have the repro handy, so it's a natural fit. I'll reference your |
|
Thanks — and the miscount was a useful one to have made. Counting on a branch that already had the entry is exactly the situation the test will be in from here on, so it surfaced the asymmetry faster than counting on Glad the issue is going to you. A repro from an enable-everything sweep is better evidence than the exclusion constant on its own, since it shows the entry failing the way a user would hit it rather than the way a test avoids it. I'll link the issue from Looking forward to #2218 landing. |
|
It's up: #2222. Thanks again. |
Change Description
RecognizerListLoader.getinstantiates every predefined recognizer with anamekeyword argument, taken from the YAML entry — or fromnamewhenclass_namesupplies the class, which is the documented rename path (seeRecognizerListLoader.get_recognizer_name). A recognizer whose__init__does not acceptnametherefore cannot be built from a registry configuration at all. Four recognizers were in that state.Three are listed in
default_recognizers.yamlwithenabled: falseand cannot be turned on.KrBrnRecognizer,KrDriverLicenseRecognizerandUsMbiRecognizerraiseTypeError: __init__() got an unexpected keyword argument 'name'before the registry finishes loading.enabled: falseis an opt-in switch, not a disclaimer — an entry that cannot be enabled should not be listed. The failure also reads as a user configuration error even though the YAML is correct, unlike the optional-dependency entries, which refuse to load with an actionableImportErrornaming what to install.One is absent from the YAML because of the same defect.
KrPassportRecognizerwas the only Korean recognizer missing fromdefault_recognizers.yaml; listing it would have made the registry raise. It also defaulted tosupported_language="kr"while every other Korean recognizer defaults toko(ISO 639-1). #1742 migrated the Korean recognizers fromkrtoko; #1814 added this one afterwards and reintroducedkr, so anAnalyzerEnginerunningkosilently skipped it. That half only affects direct instantiation — when an entry omitssupported_languagesthe loader passes the registry's language explicitly, so the class default never applies on the YAML path.Measured across the whole shipped configuration, every entry forced to
enabled: trueand loaded:Two of the five are expected (
HuggingFaceNerRecognizer,BasicLangExtractRecognizer— actionableImportErrorfor an optional dependency). The other three are theTypeError.Changes
nameargument, forwarded tosuper().__init__, onKrPassportRecognizer,KrBrnRecognizer,KrDriverLicenseRecognizerandUsMbiRecognizer. Appended last in each signature, so existing positional callers are unaffected.KrPassportRecognizer's defaultsupported_languagetoko.KrPassportRecognizerindefault_recognizers.yamlwithenabled: falseandcountry_code: kr, matching its siblings.tests/test_predefined_recognizer_contract.py.Why a contract test, and why the existing ones missed this
The constructor signature is part of a contract nothing enforced, and the existing tests miss it from both sides:
nameis passed. They all pass.test_predefined_pattern_recognizers_have_the_right_regex_flags,test_default_recognizers_yaml_country_code_matches_class— build the default configuration, in which ~60 entries are disabled and therefore never constructed.The new tests close the gap from both directions:
PatternRecognizersubclass must accept the kwargs the loader passes. This fires when a class is added, before it reaches the YAML at all — the point at whichKrPassportRecognizerwent wrong in feat: Add Korean passport number recognizer (KR_PASSPORT) #1814.default_recognizers.yamlmust resolve to a class and must load once enabled, exercised entry by entry so a failure names the recognizer. Entries gated behind an optional dependency are skipped, since refusing to load with an actionableImportErroris their intended behavior.class_name+nameentry must produce an instance with the configured name. This is the documented reason the loader passesname, so it records why the kwarg contract is load-bearing rather than incidental.Scope of the load test
The load test covers non-pattern entries too, deliberately. Narrowing it to
PatternRecognizersubclasses would silently dropPhoneRecognizer,ZaMobileNumberRecognizerandZaTelephoneNumberRecognizer, which are notPatternRecognizers and do load.One entry is excluded by name, with its reason recorded next to the exclusion. A further test asserts every exclusion still matches a shipped entry, so the list cannot rot if an entry is renamed, removed, or fixed:
HuggingFaceNerRecognizercannot load from its shipped entry even with its dependencies installed.EntityRecognizer.__init__callsload()unconditionally,load()requiresmodel_name, and the entry does not supply one — so withtransformersandtorchpresent it raisesValueError: model_name must be set before calling load(), not theImportErrorit raises when they are absent. Supplyingmodel_namein the YAML would make the test download a model. The entry stays covered by the resolve test. This is a pre-existing defect in the entry (HuggingFaceNerRecognizeris not usable from the shipped configuration onmaineither); worth a separate issue.BasicLangExtractRecognizeris skipped only onImportError, since refusing to load without thelangextractextra is that recognizer's intended behavior. The skip is scoped to that name rather than to the exception type, so anImportErrorfrom any other entry is a failure instead of a green skip.That recognizer also carries a
config_paththat it resolves against the working directory, so it raisesFileNotFoundErrorwhen pytest runs from the repository root. The load test sets the working directory to the component root — the same one CI uses — instead of catching that error, because catchingFileNotFoundErrorwould turn a deleted or renamed shipped config file into a passing skip. A separate test asserts everyconfig_pathin the shipped configuration resolves to a file that exists, which holds even in an environment that cannot construct the recognizer at all. (The more robust YAML value would beconfig_path: langextract_config_basic.yaml, whichresolve_config_pathfinds from any cwd — out of scope here.)The signature test is scoped to
PatternRecognizersubclasses.AzureAILanguageRecognizeris the one remaining class that does not acceptname; it is absent fromdefault_recognizers.yamland fixes its own display name, so it is out of scope here — but it is reachable from a user config by class name and fails the same way. Pre-existing; worth tracking separately.Behavior change
The
kr→kodefault is a breaking change for direct instantiation.KrPassportRecognizer()now registers underko, so a caller analysing withlanguage="kr"no longer getsKR_PASSPORTresults. The failure is silent, not an exception: the registry raises only when the whole result set is empty, and the per-entity path just logsEntity KR_PASSPORT doesn't have the corresponding recognizer in language : kr. Migration is one line — passsupported_language="kr", or switch the call tolanguage="ko".Registry/YAML users are unaffected: the loader always passes
supported_languageexplicitly, and the new entry declares bothkoandkr. No call site in this repository passeslanguage="kr", and no shipped NLP configuration declareskr.Verification
presidio-analyzersuite run on this branch and on anorigin/mainworktree: the failure sets are identical (42 pre-existing failures from optional dependencies absent in the local environment —transformers,langextract, some spaCy models). No regressions.transformers/torchmodule globals patched to non-None, which reproduces a full-extras CI environment without installing them:258 passed, 1 skipped, no failures. An earlier revision of this branch failed there, which is what theHuggingFaceNerRecognizerexclusion above fixes.ImportErrorinto a non-excluded entry's constructor makes the load test fail rather than skip, confirming the narrowed exception scope.config_pathat a name that does not exist makes the new config-path test fail, confirming a deleted or renamed shipped file is caught rather than skipped.presidio-analyzerand from the repository root (258 passed, 1 skippedboth ways), so it is not working-directory dependent.ruff checkfrom the repo root (as CI runs it),ruff format --checkon the modified files, andgit diff --checkall pass.default_recognizers.yamldiff is an 8-line pure addition.Per the changelog policy in #2200,
CHANGELOG.mdis not modified — the earlier revision of this branch edited it, which is what made the PR conflict.Issue reference
Fixes #2176. Follows up on #1742 (Korean
kr→komigration) and #1814 (which addedKrPassportRecognizer).Checklist
🤖 Generated with Claude Code