Skip to content

fix(analyzer): let AzureAILanguageRecognizer load from a registry config, and un-mute the test that guards it - #2224

Merged
omri374 merged 1 commit into
data-privacy-stack:mainfrom
developer0hye:fix/azure-ai-language-name-kwarg
Aug 5, 2026
Merged

fix(analyzer): let AzureAILanguageRecognizer load from a registry config, and un-mute the test that guards it#2224
omri374 merged 1 commit into
data-privacy-stack:mainfrom
developer0hye:fix/azure-ai-language-name-kwarg

Conversation

@developer0hye

Copy link
Copy Markdown
Contributor

Change Description

AzureAILanguageRecognizer cannot be built from a registry configuration on main. RecognizerListLoader passes name for every entry it constructs, and passed context before that; the constructor accepts neither, so any YAML entry naming the class raises before the registry finishes loading:

RecognizerListLoader.get(
    recognizers=[{"name": "AzureAILanguageRecognizer", "type": "predefined",
                  "supported_languages": ["en"]}],
    supported_languages=["en"], global_regex_flags=26,
)
# TypeError: AzureAILanguageRecognizer.__init__() got an unexpected keyword argument 'name'

This is #1457, reported in 2024 against the context kwarg and fixed in #1458 by adding **kwargs. #1800 removed the **kwargs — correctly, since RemoteRecognizer.__init__ does not accept them either, so it never absorbed anything and only moved the same TypeError one frame deeper — but without adding the explicit parameters that were holding the loader path up. The other two recognizers that PR touched have since been repaired; ahds_recognizer.py and lm_recognizer.py both take name: Optional[str] = None today.

The regression test #1458 added was already unable to run by then, which is the second half of this PR.

Why the regression test could not fire

@pytest.mark.skipif(pytest.importorskip("azure"), reason="Optional dependency not installed")

pytest.importorskip skips when a module is missing and otherwise returns the module, which is truthy. Both branches lose:

azure present? Result
No Skipped is raised while the decorator is evaluated, i.e. at module import — the whole file drops out of collection
Yes truthy module → skipif(True)skipped, reporting Optional dependency not installed while it is installed

Measured on main:

$ python -m pytest tests --collect-only -q | tail -1
3248 tests collected      # without the azure-ai-language extra
3292 tests collected      # with it

So 44 tests — the entire module — are silently uncollected in any environment without the extra, and the two tests the marker was written for skip on every CI run, where --all-extras is used (ci.yml:74).

azure is also the wrong module to name. azure.* are namespace packages and the ahds extra populates them too, so import azure can succeed with azure.ai.textanalytics absent.

Changes

  • azure_ai_language.py — added name and context, appended last so existing positional callers are unaffected. name falls back to "Azure AI Language PII" when omitted, matching how AzureHealthDeidRecognizer resolves the same conflict between a configured name and a hardcoded display name.
  • test_analyzer_engine_provider.py — both markers now gate on importlib.util.find_spec for the module each test actually imports. A local helper swallows the ModuleNotFoundError that find_spec raises when an intermediate parent package is missing, which is what azure.health.deidentification does without the ahds extra.
  • The AHDS test additionally requires AHDS_ENDPOINT. AzureHealthDeidRecognizer.__init__ builds a client when the config supplies none, and that path raises ValueError without the variable — so the endpoint is as much a precondition as the package. tests/test_ahds_recognizer.py:26 already gates on it this way.
  • tests/conf/test_azure_ai_language_reco.yaml — dropped ta_client: "test", which no longer reaches the constructor (see follow-up 1 below), and added a second entry so the fixture covers both entry shapes: the plain form where name doubles as the class selector, and the class_name rename form. The test supplies credentials through monkeypatch.setenv instead; nothing reaches the network, as the SDK client is constructed locally and analyze is overridden.
  • tests/test_azure_ai_language_recognizer.py — three direct-construction tests for the name default, the name override, and context.

Verification

  • Reverting only azure_ai_language.py fails exactly three tests — test_name_can_be_overridden, test_context_is_accepted, test_analyzer_engine_provider_with_azure_ai_language — and nothing else.

  • Full presidio-analyzer suite, this branch against an origin/main baseline in the same environment: identical failure sets, 42 pre-existing failures/errors from optional dependencies absent locally (transformers, langextract, some spaCy models). Compared line by line, not by count.

    main:   26 failed, 3168 passed, 83 skipped, 16 errors
    branch: 26 failed, 3172 passed, 82 skipped, 16 errors
    

    The delta is exactly this PR: three new tests, plus the Azure AI Language test moving from skipped to passing.

  • With the extra uninstalled and site-packages/azure removed, pytest tests --collect-only reports 3295 on this branch against 3248 on main. The module is collected, its 41 other tests run, and only the two optional-dependency tests skip, with accurate reasons.

  • ruff check from the repo root as CI runs it, ruff format --check on the modified module, and git diff --check all pass.

Reproducing the "extra absent" rows takes more than pip uninstall azure-ai-textanalytics azure-core: azure-common also claims the azure namespace, and pip leaves an empty site-packages/azure/ai/ behind. Either keeps import azure succeeding.

Follow-ups, not in this PR

  1. PredefinedRecognizerConfig silently drops recognizer-specific kwargs. It inherits pydantic's default extra="ignore", so ta_client — and azure_ai_key / azure_ai_endpoint, which users would reasonably put in YAML — never reach the constructor. HuggingFaceRecognizerConfig, GLiNERRecognizerConfig and LangExtractRecognizerConfig each set extra="allow" to work around this one class at a time. Worth a decision on whether that should be the default for predefined entries.
  2. AzureHealthDeidRecognizer does not accept context either. Same shape as the name gap fixed here, and the same YAML path reaches it. Left alone to keep this diff scoped.
  3. AzureAILanguageRecognizer is still absent from default_recognizers.yaml, so fix(analyzer): make every recognizer listed in default_recognizers.yaml loadable #2170's contract tests do not reach it — the load test only walks shipped entries, and the signature test only covers PatternRecognizer subclasses. Adding a shipped entry is a separate question, since the recognizer needs credentials to construct.

Per the changelog policy in #2200, CHANGELOG.md is not modified.

Issue reference

Closes #2223. Follows up on #1457 / #1458 (the original report and fix), #1521 (which introduced the marker), #1800 (which removed the **kwargs) and #1819 (which made the loader pass name).

Checklist

  • I have reviewed the contribution guidelines
  • I agree to follow this project's Code of Conduct
  • I confirm that I have the right to submit this contribution and that it does not knowingly contain proprietary or confidential code.
  • My code includes unit tests
  • All unit tests and lint checks pass locally
  • My PR contains documentation updates / additions if required

🤖 Generated with Claude Code

RecognizerListLoader passes `name` for every entry it builds, and passed
`context` before that. `AzureAILanguageRecognizer.__init__` accepted
neither, so any YAML entry naming the class raised TypeError before the
registry finished loading. That is data-privacy-stack#1457, fixed in data-privacy-stack#1458 with `**kwargs`
and reintroduced by data-privacy-stack#1800 when the `**kwargs` came out without explicit
parameters replacing it.

Adds both parameters, appended last so existing positional callers are
unaffected. `name` falls back to "Azure AI Language PII" when omitted,
matching how AzureHealthDeidRecognizer handles the same situation.

The regression test data-privacy-stack#1458 added could not catch this. Its marker,
`skipif(pytest.importorskip("azure"), ...)`, skips whichever way the
dependency goes: absent, importorskip raises while the decorator is
evaluated and takes the whole module out of collection; present, it
returns a truthy module and skipif fires. Measured on main, `pytest
tests --collect-only` yields 3248 without the extra against 3292 with
it -- 44 tests, the entire file, silently uncollected.

Both markers now gate on importlib.util.find_spec for the module each
test actually imports. The AHDS test additionally needs AHDS_ENDPOINT,
since its recognizer builds a client from that variable when the config
supplies none.

The YAML fixture drops `ta_client: "test"`, which no longer reaches the
constructor -- PredefinedRecognizerConfig ignores extra keys -- and gains
a second entry so the fixture covers both the plain form and the
class_name rename form.

Closes data-privacy-stack#2223

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@omri374 omri374 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@omri374
omri374 merged commit 7b4111c into data-privacy-stack:main Aug 5, 2026
38 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Two tests in test_analyzer_engine_provider.py can never run, hiding a regression in AzureAILanguageRecognizer

2 participants