fix(types): stop the deprecation decorators erasing wrapped signatures - #739
Merged
Merged
Conversation
deprecated_argument, deprecated_function and deprecated_class all returned a bare Callable and wrapped the target in an untyped *args/**kwargs closure. That erased the signature of every decorated function, so mypy silently stopped checking their call sites and every override relationship they took part in — 47 @deprecated_argument applications across redisvl/, 36 of them on the vectorizer embed/embed_many surface, plus 10 more between deprecated_function and deprecated_class. Both SearchIndex constructors are among them. Retype the three decorators with ParamSpec so each wrapper keeps the wrapped signature, and give deprecated_class a type-bound TypeVar so it is checked as a class decorator. This commit is the retyping alone; mypy now reports the pre-existing errors it had been hiding, which the next commit fixes.
With the deprecation decorators now signature-preserving, mypy checks the
vectorizer override hierarchy again and reports 52 pre-existing errors. Two
genuine defects were behind them.
BaseVectorizer._embed declared `text` as its first parameter and `content` as
its second, but embed() dispatches positionally — `self._embed(content, ...)`.
Every provider happens to name its first parameter `content`, so this worked;
any implementation that followed the base signature instead would have silently
received the content in `text`. The four provider hooks (_embed, _embed_many,
_aembed, _aembed_many) are internal extension points: the public methods
already resolve the deprecated `text`/`texts` alias before dispatching, and
nothing in the codebase ever calls them with it. Give them the signature they
are actually called with — content first and required, no alias — and drop the
now-dead alias parameters and @deprecated_argument decorators from the five
providers that carried them. embed_many/aembed_many dispatched the batch hooks
by keyword while embed/aembed dispatched positionally; make all four
positional so one contract covers the set.
One behavioural edge follows from dropping the aliases: `_embed_many([])` used
to resolve `[] or None` to None and raise TypeError, and now returns []. The
public embed_many/aembed_many short-circuit empty input before dispatching, so
this is unreachable from the public API.
The four deprecated *TextVectorizer aliases each re-implemented embed and
embed_many purely to merge `text` into `content` and warn, which BaseVectorizer
already does identically. In doing so they narrowed the contract: they dropped
preprocess/batch_size/as_buffer/skip_cache from the positional signature, so
`BedrockTextVectorizer().embed("hi", None, fn)` raised TypeError where the
parent works, and most of them annotated a `list[float]` return that is bytes
whenever as_buffer=True. Delete the redundant overrides; the aliases inherit
correct, honestly typed methods and still warn on both the class and the `text`
kwarg.
One of the 52 errors was outside the vectorizer hierarchy: BaseCache.redis_kwargs
was an inferred heterogeneous dict, so BaseCache cast on every read of it and
SemanticCache passed its values into SearchIndex unchecked. Typing it as a
TypedDict removes the four casts; semantic.py needs no change and now
type-checks as it stands.
vishal-bala
marked this pull request as ready for review
September 28, 2026 12:33
One conflict, in the import block of redisvl/extensions/cache/base.py. main dropped the collections.abc.Mapping import when clear()/aclear() moved onto scan_iter; this branch replaced the typing.cast import with TypedDict when redis_kwargs gained a type. Neither Mapping nor cast is referenced after the merge, so the resolution keeps `from typing import Any, TypedDict` alone.
limjoobin
approved these changes
Oct 2, 2026
limjoobin
left a comment
Contributor
There was a problem hiding this comment.
LGTM. This PR ensures the deprecation decorators remain consistent with their real signatures for mypy type safety checking by retyping them with ParamSpec/TypeVar instead of Callable.
Just a few merge conflicts to resolve and this PR is good to go!
One conflict, in redisvl/extensions/cache/base.py, resolved by taking main's version of the file. main (#726, #727) independently typed BaseCache.redis_kwargs as a TypedDict, _CacheConnectionKwargs, with the same three fields, and removed the same four casts. This branch's RedisConnectionKwargs was the only change it made to that file, so keeping both would have left two identical types. utils.py merged cleanly: main reworded the three deprecation messages ("a future release" rather than "the next major release"), and the ParamSpec retyping of the same functions is untouched.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
The three deprecation decorators in
redisvl/utils/utils.pyreturned a bareCallableand wrapped their target in an untyped*args/**kwargsclosure. A bareCallablemeansCallable[..., Any], so every decorated function lost its signature: mypy accepted any arguments at any call site, treated every return value asAny, and skipped the override checks those methods took part in, becauseCallable[..., Any]is compatible with any signature.That covered 47
@deprecated_argumentapplications acrossredisvl/, 36 of them on the vectorizerembed/embed_manysurface, plus 10 more betweendeprecated_functionanddeprecated_class. BothSearchIndexconstructors were among them.make check-typespassed the whole time.The hole was found by accident. Removing
@deprecated_argumentfromSearchIndex.__init__andAsyncSearchIndex.__init__immediately surfaced fourarg-typeerrors inredisvl/extensions/cache/llm/semantic.pythat had been invisible for as long as the decorator had been applied.Changes
Retype the decorators with ParamSpec
deprecated_argument,deprecated_functionanddeprecated_classnow preserve what they wrap. AParamSpeccarries the parameter list and aTypeVarcarries the return type, so the decorated name keeps the signature it had:The wrapper is annotated
*args: P.args, **kwargs: P.kwargsto tie it to the same parameter list.deprecated_classtakesC = TypeVar("C", bound=type)and returnsCallable[[C], C], so a decorated class keeps its own type and the decorator is checked as a class decorator. Theclassmethod/staticmethodbranch needs twocastcalls, because aclassmethodobject is not aCallableto the type system; that branch covers the decorator ordering the docstring documents as unsupported.Restoring the signatures surfaced 52 pre-existing errors. The retyping is committed on its own so the diff stays readable, which means
check-typesfails on that commit and passes on the next.Give the vectorizer provider hooks the signature they are called with
BaseVectorizer._embeddeclaredtextas its first parameter andcontentas its second, whileembed()dispatches positionally asself._embed(content, **kwargs). Every provider in the tree happens to name its first parametercontent, so this worked; an implementation that followed the base signature instead would have silently received the content intext.The four hooks (
_embed,_embed_many,_aembed,_aembed_many) are internal extension points, and the public methods resolve the deprecatedtext/textsalias before dispatching to them. Nothing in the tree ever calls a hook with the alias. They now take content first and required, with no alias, and the dead alias parameters and@deprecated_argumentdecorators are gone from the five providers that carried them:embed_manyandaembed_manydispatched the batch hooks by keyword whileembedandaembeddispatched positionally. All four are positional now, so one contract covers the set: the content is always the first positional argument, which leaves a provider free to name that parameter whatever it likes.Delete the redundant overrides on the deprecated alias classes
CustomTextVectorizer,BedrockTextVectorizer,VertexAITextVectorizerandVoyageAITextVectorizereach re-implementedembedandembed_manyto mergetextintocontentand warn, whichBaseVectorizeralready does identically. In doing so they narrowed the contract they existed to preserve. They droppedpreprocess,batch_size,as_bufferandskip_cachefrom the positional signature, soBedrockTextVectorizer().embed("hi", None, fn)raisedTypeErrorwhere the parent works, and most of them annotated alist[float]return that isbyteswheneveras_buffer=True.The overrides are deleted. Each alias now inherits the parent's full, honestly typed methods, and still warns on both the class and the
textkeyword.Type
BaseCache.redis_kwargsOne of the 52 errors was outside the vectorizer hierarchy.
redis_kwargswas an inferred heterogeneous dict, which is whyBaseCachecast on every read of it and whySemanticCachecould pass its values intoSearchIndexunchecked. ARedisConnectionKwargsTypedDict removes the four casts.semantic.pyis unchanged and now type-checks as it stands.Release Notes
Type checkers now see real signatures for the vectorizer
embedandembed_manymethods, bothSearchIndexconstructors,SemanticCache,SemanticRouterandMessageHistory, all of which the deprecation decorators previously erased. Calls passing arguments those functions do not accept may newly fail type checking.Notes
Anyone subclassing
BaseVectorizeroutside this repository to implement the private hooks is affected in one direction only. A subclass whose first parameter is namedcontentkeeps working, whatever else it declares, and so does one that names it something else, because every dispatch is positional. A subclass that declared the base's old(text, content)order was already receiving the content intextand is now flagged by a type checker rather than failing silently. The documented way to supply your own embedding function isCustomVectorizer, which is untouched.Dropping the alias parameters changes one edge.
_embed_many([])used to resolve[] or NonetoNoneand raiseTypeError; it now returns[]. The publicembed_manyandaembed_manyshort-circuit empty input before dispatching, so this is unreachable from the public API.Verified locally on Python 3.13 with all extras installed:
mypy ./redisvlclean across 120 source files, 1524 unit tests, 63 LLM-cache integration tests, and 34 vectorizer integration tests with the 75 that need provider API keys skipped. No test file is modified by this change, so the passing suite is a behaviour-preservation signal rather than tests adapted to new behaviour. The pre-existing mypy error atredisvl/index/storage.py:630did not reproduce in this environment and is untouched either way.Note
Medium Risk
Mostly static typing and internal hook contracts; alias-class overrides removal fixes prior signature bugs but may change type-checker results for consumers relying on erased signatures.
Overview
Deprecation decorators (
deprecated_argument,deprecated_function,deprecated_class) are retyped withParamSpecand return-typeTypeVars so wrapped callables keep their real signatures instead of collapsing to untypedCallable.deprecated_classusesgetattr/setattron__init__for mypy-friendly class decoration.Vectorizer internals:
BaseVectorizer’s_embed/_embed_many/_aembed/_aembed_manyhooks now take required content as the first positional argument only—deprecatedtext/textshandling stays on the publicembed*methods. Batch hooks are invoked positionally (notcontents=keyword). Provider implementations drop duplicate@deprecated_argumentand alias parameters on those private methods.Deprecated alias classes (
BedrockTextVectorizer,CustomTextVectorizer,VertexAITextVectorizer,VoyageAITextVectorizer) no longer overrideembed/embed_manywith narrowed signatures; they inheritBaseVectorizer’s full API whiletext/textsstill warn on the public surface.Reviewed by Cursor Bugbot for commit da3f290. Bugbot is set up for automated code reviews on this repo. Configure here.