Skip to content

The pydantic-free CLI JSON fallback does not exist #116

Description

@tony

Summary

AGENTS.md requires the CLI JSON path to keep working when Pydantic cannot be imported:

the CLI JSON path must keep its pydantic-free fallback so basic search output remains available when Pydantic cannot be imported

That fallback does not exist. Blocking Pydantic makes import agentgrep raise ImportError before any serializer is ever selected.

Worse, the regression test that was supposed to protect it passed green against a fallback that is unreachable.

Present in v0.1.0a33.

Reproduction

$ python -c "
import builtins
_real = builtins.__import__
def guard(name, *a, **k):
    if name == 'pydantic' or name.startswith('pydantic.'):
        raise ImportError('pydantic blocked')
    return _real(name, *a, **k)
builtins.__import__ = guard
import agentgrep
"
ImportError: pydantic blocked

The package does not import at all, so the JSON serializer's except ImportError branch is never reached.

Why the existing test cannot catch it

test_json_output_falls_back_without_pydantic monkeypatches importlib.import_module inside an already-imported package. By the time it runs, every module-scope import pydantic has already been satisfied.

It therefore exercises the except ImportError inside the serializer-selection helper and proves nothing about the real import graph. It is a test asserting a property it structurally cannot observe.

The actual dependency

Two things make Pydantic mandatory.

A dead import at the package root

src/agentgrep/__init__.py#L60 is a bare import pydantic with no pydantic.* reference anywhere in the module body. It does nothing except make the whole package un-importable without Pydantic.

(This one is already deleted on the #112 branch — necessary, but not sufficient.)

Four modules on the search path subclass pydantic.BaseModel at module scope

Module Models
stores.py DiscoverySpec, StoreDescriptor, StoreCatalog
progress.py the six event payloads
query/ast.py Token, the node union
events.py the SearchEvent union

Each was confirmed individually — with Pydantic blocked, importing any one of them raises.

So Pydantic is not an optional adapter at the boundary today. It is a hard, load-bearing runtime dependency of the core — the opposite of what AGENTS.md describes, and a direct contradiction of the stated policy that Pydantic is "a schema, validation, and adapter layer at explicit boundaries" rather than the semantic source of truth.

The fix

Give the four modules a Pydantic-free definition at module scope, and keep Pydantic where the policy says it belongs — at the MCP schema boundary, imported lazily.

events.py is the most tractable and the most instructive. Its models already set arbitrary_types_allowed=True so they can embed plain dataclasses — which means they cannot model_dump_json() anyway. Pydantic is buying discriminator metadata that isinstance already provides. There is little to lose and a hard dependency to shed.

stores.py is the load-bearing one. The catalog descriptors are the widest surface and they are constructed at import time.

Two properties to preserve while doing it:

  • The cold-start budget (agentgrep --help under 250 ms). Dropping four eager Pydantic model registrations should help rather than hurt, but it should be measured rather than assumed.
  • The frozen/validated semantics the catalog relies on. A @dataclass(frozen=True) gives immutability but not validation; whatever replaces BaseModel must not quietly loosen a guarantee the catalog tests depend on.

Acceptance

The two subprocess boundary tests added in #112 (test_cli_json_survives_missing_pydantic, for both json and ndjson) stop being xfail and pass on their own merits.

They run the real CLI in a real subprocess with Pydantic genuinely absent — so nothing else is needed to prove the fix. They are currently xfail(strict=True), which means the moment the fallback works they turn red until the marker is removed. This cannot be silently closed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions