Skip to content

Commit 2df93a6

Browse files
committed
Support classmethods
1 parent af2c028 commit 2df93a6

File tree

2 files changed

+240
-53
lines changed

2 files changed

+240
-53
lines changed

redisvl/utils/utils.py

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import inspect
22
import json
3+
import warnings
4+
from contextlib import ContextDecorator, contextmanager
35
from enum import Enum
46
from functools import wraps
57
from time import time
@@ -68,24 +70,59 @@ def deprecated_argument(argument: str, replacement: Optional[str] = None) -> Cal
6870
6971
When the wrapped function is called, the decorator will warn if the
7072
deprecated argument is passed as an argument or keyword argument.
71-
"""
7273
74+
NOTE: The @deprecated_argument decorator should not fall "outside"
75+
of the @classmethod decorator, but instead should come between
76+
it and the method definition. For example:
77+
78+
class MyClass:
79+
@classmethod
80+
@deprecated_argument("old_arg", "new_arg")
81+
@other_decorator
82+
def test_method(cls, old_arg=None, new_arg=None):
83+
pass
84+
"""
7385
message = f"Argument {argument} is deprecated and will be removed in the next major release."
7486
if replacement:
7587
message += f" Use {replacement} instead."
7688

77-
def wrapper(func):
78-
@wraps(func)
79-
def inner(*args, **kwargs):
80-
argument_names = list(inspect.signature(func).parameters.keys())
89+
def decorator(func):
90+
# Check if the function is a classmethod or staticmethod
91+
if isinstance(func, (classmethod, staticmethod)):
92+
underlying = func.__func__
93+
94+
@wraps(underlying)
95+
def inner_wrapped(*args, **kwargs):
96+
sig = inspect.signature(underlying)
97+
bound_args = sig.bind(*args, **kwargs)
98+
if argument in bound_args.arguments:
99+
warn(message, DeprecationWarning, stacklevel=2)
100+
return underlying(*args, **kwargs)
101+
102+
if isinstance(func, classmethod):
103+
return classmethod(inner_wrapped)
104+
else:
105+
return staticmethod(inner_wrapped)
106+
else:
81107

82-
if argument in argument_names:
83-
warn(message, DeprecationWarning, stacklevel=2)
84-
elif argument in kwargs:
85-
warn(message, DeprecationWarning, stacklevel=2)
108+
@wraps(func)
109+
def inner_normal(*args, **kwargs):
110+
sig = inspect.signature(func)
111+
bound_args = sig.bind(*args, **kwargs)
112+
if argument in bound_args.arguments:
113+
warn(message, DeprecationWarning, stacklevel=2)
114+
return func(*args, **kwargs)
86115

87-
return func(*args, **kwargs)
116+
return inner_normal
88117

89-
return inner
118+
return decorator
90119

91-
return wrapper
120+
121+
@contextmanager
122+
def assert_no_warnings():
123+
"""
124+
Assert that a function does not emit any warnings when called.
125+
"""
126+
with warnings.catch_warnings():
127+
warnings.simplefilter("error")
128+
yield

tests/unit/test_utils.py

Lines changed: 191 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import warnings
2+
from functools import wraps
3+
24
import numpy as np
35
import pytest
46

@@ -8,7 +10,7 @@
810
convert_bytes,
911
make_dict,
1012
)
11-
from redisvl.utils.utils import deprecated_argument
13+
from redisvl.utils.utils import assert_no_warnings, deprecated_argument
1214

1315

1416
def test_even_number_of_elements():
@@ -149,104 +151,252 @@ def test_conversion_with_invalid_floats():
149151
assert len(result) > 0 # Simple check to ensure it returns anything
150152

151153

154+
def decorator(func):
155+
@wraps(func)
156+
def wrapper(*args, **kwargs):
157+
print("boop")
158+
return func(*args, **kwargs)
159+
160+
return wrapper
161+
162+
152163
class TestDeprecatedArgument:
153164
def test_deprecation_warning_text_with_replacement(self):
154-
@deprecated_argument("dtype", "vectorizer")
155-
def test_func(dtype=None, vectorizer=None):
165+
@deprecated_argument("old_arg", "new_arg")
166+
def test_func(old_arg=None, new_arg=None, neutral_arg=None):
156167
pass
157168

169+
# Test that passing the deprecated argument as a keyword triggers the warning.
158170
with pytest.warns(DeprecationWarning) as record:
159-
test_func(dtype="float32")
171+
test_func(old_arg="float32")
160172

161173
assert len(record) == 1
162174
assert str(record[0].message) == (
163-
"Argument dtype is deprecated and will be removed"
164-
" in the next major release. Use vectorizer instead."
175+
"Argument old_arg is deprecated and will be removed in the next major release. Use new_arg instead."
165176
)
166177

178+
# Test that passing the deprecated argument as a positional argument also triggers the warning.
179+
with pytest.warns(DeprecationWarning) as record:
180+
test_func("float32", neutral_arg="test_vector")
181+
182+
assert len(record) == 1
183+
assert str(record[0].message) == (
184+
"Argument old_arg is deprecated and will be removed in the next major release. Use new_arg instead."
185+
)
186+
187+
with assert_no_warnings():
188+
test_func(new_arg="float32")
189+
test_func(new_arg="float32", neutral_arg="test_vector")
190+
167191
def test_deprecation_warning_text_without_replacement(self):
168-
@deprecated_argument("dtype")
169-
def test_func(dtype=None):
192+
@deprecated_argument("old_arg")
193+
def test_func(old_arg=None, neutral_arg=None):
170194
pass
171195

196+
# As a kwarg
172197
with pytest.warns(DeprecationWarning) as record:
173-
test_func(dtype="float32")
198+
test_func(old_arg="float32")
174199

175200
assert len(record) == 1
176201
assert str(record[0].message) == (
177-
"Argument dtype is deprecated and will be removed"
202+
"Argument old_arg is deprecated and will be removed"
178203
" in the next major release."
179204
)
180205

181-
def test_function_argument(self):
182-
@deprecated_argument("dtype", "vectorizer")
183-
def test_func(dtype=None, vectorizer=None):
206+
# As a positional arg
207+
with pytest.warns(DeprecationWarning):
208+
test_func("float32", neutral_arg="test_vector")
209+
210+
assert len(record) == 1
211+
assert str(record[0].message) == (
212+
"Argument old_arg is deprecated and will be removed"
213+
" in the next major release."
214+
)
215+
216+
with assert_no_warnings():
217+
test_func(neutral_arg="test_vector")
218+
219+
def test_function_positional_argument_required(self):
220+
"""
221+
NOTE: In this situation, it's not possible to avoid a deprecation
222+
warning because the argument is currently required.
223+
"""
224+
225+
@deprecated_argument("old_arg")
226+
def test_func(old_arg, neutral_arg):
227+
pass
228+
229+
with pytest.warns(DeprecationWarning):
230+
test_func("float32", "bob")
231+
232+
def test_function_positional_argument_optional(self):
233+
@deprecated_argument("old_arg")
234+
def test_func(neutral_arg, old_arg=None):
184235
pass
185236

186237
with pytest.warns(DeprecationWarning):
187-
test_func(dtype="float32")
238+
test_func("bob", "float32")
239+
240+
with assert_no_warnings():
241+
test_func("bob")
188242

189243
def test_function_keyword_argument(self):
190-
@deprecated_argument("dtype", "vectorizer")
191-
def test_func(dtype=None, vectorizer=None):
244+
@deprecated_argument("old_arg", "new_arg")
245+
def test_func(old_arg=None, new_arg=None):
192246
pass
193247

194248
with pytest.warns(DeprecationWarning):
195-
test_func(vectorizer="float32")
249+
test_func(old_arg="float32")
250+
251+
with assert_no_warnings():
252+
test_func(new_arg="float32")
253+
254+
def test_function_keyword_argument_multiple_decorators(self):
255+
@deprecated_argument("old_arg", "new_arg")
256+
@decorator
257+
def test_func(old_arg=None, new_arg=None):
258+
pass
259+
260+
with pytest.warns(DeprecationWarning):
261+
test_func(old_arg="float32")
262+
263+
with assert_no_warnings():
264+
test_func(new_arg="float32")
265+
266+
def test_method_positional_argument_optional(self):
267+
class TestClass:
268+
@deprecated_argument("old_arg", "new_arg")
269+
def test_method(self, new_arg=None, old_arg=None):
270+
pass
271+
272+
with pytest.warns(DeprecationWarning):
273+
TestClass().test_method("float32", "bob")
274+
275+
with assert_no_warnings():
276+
TestClass().test_method("float32")
277+
278+
def test_method_positional_argument_required(self):
279+
"""
280+
NOTE: In this situation, it's not possible to avoid a deprecation
281+
warning because the argument is currently required.
282+
"""
283+
284+
class TestClass:
285+
@deprecated_argument("old_arg", "new_arg")
286+
def test_method(self, old_arg, new_arg):
287+
pass
288+
289+
with pytest.warns(DeprecationWarning):
290+
TestClass().test_method("float32", new_arg="bob")
291+
292+
def test_method_keyword_argument(self):
293+
class TestClass:
294+
@deprecated_argument("old_arg", "new_arg")
295+
def test_method(self, old_arg=None, new_arg=None):
296+
pass
297+
298+
with pytest.warns(DeprecationWarning):
299+
TestClass().test_method(old_arg="float32")
300+
301+
with assert_no_warnings():
302+
TestClass().test_method(new_arg="float32")
303+
304+
def test_classmethod_positional_argument_required(self):
305+
"""
306+
NOTE: In this situation, it's impossible to avoid a deprecation
307+
warning because the argument is currently required.
308+
"""
196309

197-
def test_class_method_argument(self):
198310
class TestClass:
199-
@deprecated_argument("dtype", "vectorizer")
200-
def test_method(self, dtype=None, vectorizer=None):
311+
@deprecated_argument("old_arg", "new_arg")
312+
@classmethod
313+
def test_method(cls, old_arg, new_arg):
201314
pass
202315

203316
with pytest.warns(DeprecationWarning):
204-
TestClass().test_method(dtype="float32")
317+
TestClass.test_method("float32", new_arg="bob")
205318

206-
def test_class_method_keyword_argument(self):
319+
def test_classmethod_positional_argument_optional(self):
207320
class TestClass:
208-
@deprecated_argument("dtype", "vectorizer")
209-
def test_method(self, dtype=None, vectorizer=None):
321+
@deprecated_argument("old_arg", "new_arg")
322+
@classmethod
323+
def test_method(cls, new_arg=None, old_arg=None):
210324
pass
211325

212326
with pytest.warns(DeprecationWarning):
213-
TestClass().test_method(vectorizer="float32")
327+
TestClass.test_method("float32", "bob")
328+
329+
with assert_no_warnings():
330+
TestClass.test_method("float32")
331+
332+
def test_classmethod_keyword_argument(self):
333+
class TestClass:
334+
@deprecated_argument("old_arg", "new_arg")
335+
@classmethod
336+
def test_method(cls, old_arg=None, new_arg=None):
337+
pass
338+
339+
with pytest.warns(DeprecationWarning):
340+
TestClass.test_method(old_arg="float32")
341+
342+
with assert_no_warnings():
343+
TestClass.test_method(new_arg="float32")
344+
345+
def test_classmethod_keyword_argument_multiple_decorators(self):
346+
"""
347+
NOTE: The @deprecated_argument decorator should come between @classmethod
348+
and the method definition.
349+
"""
350+
351+
class TestClass:
352+
@classmethod
353+
@deprecated_argument("old_arg", "new_arg")
354+
@decorator
355+
def test_method(cls, old_arg=None, new_arg=None):
356+
pass
357+
358+
with pytest.warns(DeprecationWarning):
359+
TestClass.test_method(old_arg="float32")
360+
361+
with assert_no_warnings():
362+
TestClass.test_method(new_arg="float32")
214363

215364
def test_class_init_argument(self):
216365
class TestClass:
217-
@deprecated_argument("dtype", "vectorizer")
218-
def __init__(self, dtype=None, vectorizer=None):
366+
@deprecated_argument("old_arg", "new_arg")
367+
def __init__(self, old_arg=None, new_arg=None):
219368
pass
220369

221370
with pytest.warns(DeprecationWarning):
222-
TestClass(dtype="float32")
371+
TestClass(old_arg="float32")
223372

224373
def test_class_init_keyword_argument(self):
225374
class TestClass:
226-
@deprecated_argument("dtype", "vectorizer")
227-
def __init__(self, dtype=None, vectorizer=None):
375+
@deprecated_argument("old_arg", "new_arg")
376+
def __init__(self, old_arg=None, new_arg=None):
228377
pass
229378

230379
with pytest.warns(DeprecationWarning):
231-
TestClass(dtype="float32")
380+
TestClass(old_arg="float32")
381+
382+
with assert_no_warnings():
383+
TestClass(new_arg="float32")
232384

233385
async def test_async_function_argument(self):
234-
@deprecated_argument("dtype", "vectorizer")
235-
async def test_func(dtype=None, vectorizer=None):
386+
@deprecated_argument("old_arg", "new_arg")
387+
async def test_func(old_arg=None, new_arg=None):
236388
return 1
237389

238390
with pytest.warns(DeprecationWarning):
239-
result = await test_func(dtype="float32")
391+
result = await test_func(old_arg="float32")
240392
assert result == 1
241-
393+
242394
async def test_ignores_local_variable(self):
243-
@deprecated_argument("dtype")
244-
async def test_func(vectorizer=None):
395+
@deprecated_argument("old_arg", "new_arg")
396+
async def test_func(old_arg=None, new_arg=None):
245397
# The presence of this variable should not trigger a warning
246-
dtype = "float32"
398+
old_arg = "float32"
247399
return 1
248400

249-
# This will raise an error if any warning is emitted
250-
with warnings.catch_warnings():
251-
warnings.simplefilter("error")
401+
with assert_no_warnings():
252402
await test_func()

0 commit comments

Comments
 (0)