Skip to content

Commit c54880a

Browse files
authored
Avoid looking at local variables for deprecation warnings (#283)
Deprecation warnings should not be looking at local function variables.
1 parent f56e610 commit c54880a

File tree

2 files changed

+265
-46
lines changed

2 files changed

+265
-46
lines changed

redisvl/utils/utils.py

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
import inspect
12
import json
3+
import warnings
4+
from contextlib import ContextDecorator, contextmanager
25
from enum import Enum
36
from functools import wraps
47
from time import time
@@ -67,24 +70,65 @@ def deprecated_argument(argument: str, replacement: Optional[str] = None) -> Cal
6770
6871
When the wrapped function is called, the decorator will warn if the
6972
deprecated argument is passed as an argument or keyword argument.
70-
"""
7173
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+
"""
7285
message = f"Argument {argument} is deprecated and will be removed in the next major release."
7386
if replacement:
7487
message += f" Use {replacement} instead."
7588

76-
def wrapper(func):
77-
@wraps(func)
78-
def inner(*args, **kwargs):
79-
argument_names = func.__code__.co_varnames
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+
if argument in kwargs:
97+
warn(message, DeprecationWarning, stacklevel=2)
98+
else:
99+
sig = inspect.signature(underlying)
100+
bound_args = sig.bind(*args, **kwargs)
101+
if argument in bound_args.arguments:
102+
warn(message, DeprecationWarning, stacklevel=2)
103+
return underlying(*args, **kwargs)
104+
105+
if isinstance(func, classmethod):
106+
return classmethod(inner_wrapped)
107+
else:
108+
return staticmethod(inner_wrapped)
109+
else:
80110

81-
if argument in argument_names:
82-
warn(message, DeprecationWarning, stacklevel=2)
83-
elif argument in kwargs:
84-
warn(message, DeprecationWarning, stacklevel=2)
111+
@wraps(func)
112+
def inner_normal(*args, **kwargs):
113+
if argument in kwargs:
114+
warn(message, DeprecationWarning, stacklevel=2)
115+
else:
116+
sig = inspect.signature(func)
117+
bound_args = sig.bind(*args, **kwargs)
118+
if argument in bound_args.arguments:
119+
warn(message, DeprecationWarning, stacklevel=2)
120+
return func(*args, **kwargs)
85121

86-
return func(*args, **kwargs)
122+
return inner_normal
87123

88-
return inner
124+
return decorator
89125

90-
return wrapper
126+
127+
@contextmanager
128+
def assert_no_warnings():
129+
"""
130+
Assert that a function does not emit any warnings when called.
131+
"""
132+
with warnings.catch_warnings():
133+
warnings.simplefilter("error")
134+
yield

tests/unit/test_utils.py

Lines changed: 209 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import warnings
2+
from functools import wraps
3+
14
import numpy as np
25
import pytest
36

@@ -7,7 +10,7 @@
710
convert_bytes,
811
make_dict,
912
)
10-
from redisvl.utils.utils import deprecated_argument
13+
from redisvl.utils.utils import assert_no_warnings, deprecated_argument
1114

1215

1316
def test_even_number_of_elements():
@@ -156,92 +159,264 @@ def test_conversion_with_invalid_floats():
156159
assert len(result) > 0 # Simple check to ensure it returns anything
157160

158161

162+
def decorator(func):
163+
@wraps(func)
164+
def wrapper(*args, **kwargs):
165+
print("boop")
166+
return func(*args, **kwargs)
167+
168+
return wrapper
169+
170+
159171
class TestDeprecatedArgument:
160172
def test_deprecation_warning_text_with_replacement(self):
161-
@deprecated_argument("dtype", "vectorizer")
162-
def test_func(dtype=None, vectorizer=None):
173+
@deprecated_argument("old_arg", "new_arg")
174+
def test_func(old_arg=None, new_arg=None, neutral_arg=None):
163175
pass
164176

177+
# Test that passing the deprecated argument as a keyword triggers the warning.
178+
with pytest.warns(DeprecationWarning) as record:
179+
test_func(old_arg="float32")
180+
181+
assert len(record) == 1
182+
assert str(record[0].message) == (
183+
"Argument old_arg is deprecated and will be removed in the next major release. Use new_arg instead."
184+
)
185+
186+
# Test that passing the deprecated argument as a positional argument also triggers the warning.
165187
with pytest.warns(DeprecationWarning) as record:
166-
test_func(dtype="float32")
188+
test_func("float32", neutral_arg="test_vector")
167189

168190
assert len(record) == 1
169191
assert str(record[0].message) == (
170-
"Argument dtype is deprecated and will be removed"
171-
" in the next major release. Use vectorizer instead."
192+
"Argument old_arg is deprecated and will be removed in the next major release. Use new_arg instead."
172193
)
173194

195+
with assert_no_warnings():
196+
test_func(new_arg="float32")
197+
test_func(new_arg="float32", neutral_arg="test_vector")
198+
174199
def test_deprecation_warning_text_without_replacement(self):
175-
@deprecated_argument("dtype")
176-
def test_func(dtype=None):
200+
@deprecated_argument("old_arg")
201+
def test_func(old_arg=None, neutral_arg=None):
177202
pass
178203

204+
# As a kwarg
179205
with pytest.warns(DeprecationWarning) as record:
180-
test_func(dtype="float32")
206+
test_func(old_arg="float32")
181207

182208
assert len(record) == 1
183209
assert str(record[0].message) == (
184-
"Argument dtype is deprecated and will be removed"
210+
"Argument old_arg is deprecated and will be removed"
185211
" in the next major release."
186212
)
187213

188-
def test_function_argument(self):
189-
@deprecated_argument("dtype", "vectorizer")
190-
def test_func(dtype=None, vectorizer=None):
214+
# As a positional arg
215+
with pytest.warns(DeprecationWarning):
216+
test_func("float32", neutral_arg="test_vector")
217+
218+
assert len(record) == 1
219+
assert str(record[0].message) == (
220+
"Argument old_arg is deprecated and will be removed"
221+
" in the next major release."
222+
)
223+
224+
with assert_no_warnings():
225+
test_func(neutral_arg="test_vector")
226+
227+
def test_function_positional_argument_required(self):
228+
"""
229+
NOTE: In this situation, it's not possible to avoid a deprecation
230+
warning because the argument is currently required.
231+
"""
232+
233+
@deprecated_argument("old_arg")
234+
def test_func(old_arg, neutral_arg):
235+
pass
236+
237+
with pytest.warns(DeprecationWarning):
238+
test_func("float32", "bob")
239+
240+
def test_function_positional_argument_optional(self):
241+
@deprecated_argument("old_arg")
242+
def test_func(neutral_arg, old_arg=None):
191243
pass
192244

193245
with pytest.warns(DeprecationWarning):
194-
test_func(dtype="float32")
246+
test_func("bob", "float32")
247+
248+
with assert_no_warnings():
249+
test_func("bob")
195250

196251
def test_function_keyword_argument(self):
197-
@deprecated_argument("dtype", "vectorizer")
198-
def test_func(dtype=None, vectorizer=None):
252+
@deprecated_argument("old_arg", "new_arg")
253+
def test_func(old_arg=None, new_arg=None):
254+
pass
255+
256+
with pytest.warns(DeprecationWarning):
257+
test_func(old_arg="float32")
258+
259+
with assert_no_warnings():
260+
test_func(new_arg="float32")
261+
262+
def test_function_keyword_argument_multiple_decorators(self):
263+
@deprecated_argument("old_arg", "new_arg")
264+
@decorator
265+
def test_func(old_arg=None, new_arg=None):
199266
pass
200267

201268
with pytest.warns(DeprecationWarning):
202-
test_func(vectorizer="float32")
269+
test_func(old_arg="float32")
270+
271+
with assert_no_warnings():
272+
test_func(new_arg="float32")
273+
274+
def test_method_positional_argument_optional(self):
275+
class TestClass:
276+
@deprecated_argument("old_arg", "new_arg")
277+
def test_method(self, new_arg=None, old_arg=None):
278+
pass
279+
280+
with pytest.warns(DeprecationWarning):
281+
TestClass().test_method("float32", "bob")
282+
283+
with assert_no_warnings():
284+
TestClass().test_method("float32")
285+
286+
def test_method_positional_argument_required(self):
287+
"""
288+
NOTE: In this situation, it's not possible to avoid a deprecation
289+
warning because the argument is currently required.
290+
"""
291+
292+
class TestClass:
293+
@deprecated_argument("old_arg", "new_arg")
294+
def test_method(self, old_arg, new_arg):
295+
pass
296+
297+
with pytest.warns(DeprecationWarning):
298+
TestClass().test_method("float32", new_arg="bob")
203299

204-
def test_class_method_argument(self):
300+
def test_method_keyword_argument(self):
205301
class TestClass:
206-
@deprecated_argument("dtype", "vectorizer")
207-
def test_method(self, dtype=None, vectorizer=None):
302+
@deprecated_argument("old_arg", "new_arg")
303+
def test_method(self, old_arg=None, new_arg=None):
208304
pass
209305

210306
with pytest.warns(DeprecationWarning):
211-
TestClass().test_method(dtype="float32")
307+
TestClass().test_method(old_arg="float32")
308+
309+
with assert_no_warnings():
310+
TestClass().test_method(new_arg="float32")
311+
312+
def test_classmethod_positional_argument_required(self):
313+
"""
314+
NOTE: In this situation, it's impossible to avoid a deprecation
315+
warning because the argument is currently required.
316+
"""
317+
318+
class TestClass:
319+
@deprecated_argument("old_arg", "new_arg")
320+
@classmethod
321+
def test_method(cls, old_arg, new_arg):
322+
pass
323+
324+
with pytest.warns(DeprecationWarning):
325+
TestClass.test_method("float32", new_arg="bob")
326+
327+
def test_classmethod_positional_argument_optional(self):
328+
class TestClass:
329+
@deprecated_argument("old_arg", "new_arg")
330+
@classmethod
331+
def test_method(cls, new_arg=None, old_arg=None):
332+
pass
333+
334+
with pytest.warns(DeprecationWarning):
335+
TestClass.test_method("float32", "bob")
336+
337+
with assert_no_warnings():
338+
TestClass.test_method("float32")
212339

213-
def test_class_method_keyword_argument(self):
340+
def test_classmethod_keyword_argument(self):
214341
class TestClass:
215-
@deprecated_argument("dtype", "vectorizer")
216-
def test_method(self, dtype=None, vectorizer=None):
342+
@deprecated_argument("old_arg", "new_arg")
343+
@classmethod
344+
def test_method(cls, old_arg=None, new_arg=None):
217345
pass
218346

219347
with pytest.warns(DeprecationWarning):
220-
TestClass().test_method(vectorizer="float32")
348+
TestClass.test_method(old_arg="float32")
349+
350+
with assert_no_warnings():
351+
TestClass.test_method(new_arg="float32")
352+
353+
def test_classmethod_keyword_argument_multiple_decorators(self):
354+
"""
355+
NOTE: The @deprecated_argument decorator should come between @classmethod
356+
and the method definition.
357+
"""
358+
359+
class TestClass:
360+
@classmethod
361+
@deprecated_argument("old_arg", "new_arg")
362+
@decorator
363+
def test_method(cls, old_arg=None, new_arg=None):
364+
pass
365+
366+
with pytest.warns(DeprecationWarning):
367+
TestClass.test_method(old_arg="float32")
368+
369+
with assert_no_warnings():
370+
TestClass.test_method(new_arg="float32")
221371

222372
def test_class_init_argument(self):
223373
class TestClass:
224-
@deprecated_argument("dtype", "vectorizer")
225-
def __init__(self, dtype=None, vectorizer=None):
374+
@deprecated_argument("old_arg", "new_arg")
375+
def __init__(self, old_arg=None, new_arg=None):
226376
pass
227377

228378
with pytest.warns(DeprecationWarning):
229-
TestClass(dtype="float32")
379+
TestClass(old_arg="float32")
230380

231381
def test_class_init_keyword_argument(self):
232382
class TestClass:
233-
@deprecated_argument("dtype", "vectorizer")
234-
def __init__(self, dtype=None, vectorizer=None):
383+
@deprecated_argument("old_arg", "new_arg")
384+
def __init__(self, old_arg=None, new_arg=None):
385+
pass
386+
387+
with pytest.warns(DeprecationWarning):
388+
TestClass(old_arg="float32")
389+
390+
with assert_no_warnings():
391+
TestClass(new_arg="float32")
392+
393+
def test_class_init_keyword_argument_kwargs(self):
394+
class TestClass:
395+
@deprecated_argument("old_arg", "new_arg")
396+
def __init__(self, new_arg=None, **kwargs):
235397
pass
236398

237399
with pytest.warns(DeprecationWarning):
238-
TestClass(dtype="float32")
400+
TestClass(old_arg="float32")
401+
402+
with assert_no_warnings():
403+
TestClass(new_arg="float32")
239404

240405
async def test_async_function_argument(self):
241-
@deprecated_argument("dtype", "vectorizer")
242-
async def test_func(dtype=None, vectorizer=None):
406+
@deprecated_argument("old_arg", "new_arg")
407+
async def test_func(old_arg=None, new_arg=None):
243408
return 1
244409

245410
with pytest.warns(DeprecationWarning):
246-
result = await test_func(dtype="float32")
411+
result = await test_func(old_arg="float32")
247412
assert result == 1
413+
414+
async def test_ignores_local_variable(self):
415+
@deprecated_argument("old_arg", "new_arg")
416+
async def test_func(old_arg=None, new_arg=None):
417+
# The presence of this variable should not trigger a warning
418+
old_arg = "float32"
419+
return 1
420+
421+
with assert_no_warnings():
422+
await test_func()

0 commit comments

Comments
 (0)