/ tests / utils / test_annotations.py
test_annotations.py
  1  import re
  2  from dataclasses import dataclass, fields
  3  
  4  import pytest
  5  
  6  from mlflow.utils.annotations import _get_min_indent_of_docstring, deprecated, keyword_only
  7  
  8  # Suppress expected deprecation warnings from test fixtures
  9  pytestmark = pytest.mark.filterwarnings("ignore:.*is deprecated.*:FutureWarning")
 10  
 11  
 12  class MyClass:
 13      @deprecated()
 14      def method(self):
 15          """
 16          Returns 0
 17          """
 18          return 0
 19  
 20  
 21  @deprecated()
 22  def function():
 23      """
 24      Returns 1
 25      """
 26      return 1
 27  
 28  
 29  @keyword_only
 30  @deprecated(since="0.0.0")
 31  def deprecated_and_keyword_only_first(x):
 32      """Description
 33  
 34      Args:
 35          x: x
 36  
 37      Returns:
 38          y
 39      """
 40      return 1
 41  
 42  
 43  @deprecated(since="0.0.0")
 44  @keyword_only
 45  def deprecated_and_keyword_only_second(x):
 46      """
 47      Description
 48  
 49      Args:
 50          x: x
 51  
 52      Returns:
 53          y
 54      """
 55      return 1
 56  
 57  
 58  @deprecated()
 59  class DeprecatedClass:
 60      """
 61      A deprecated class.
 62      """
 63  
 64      def __init__(self):
 65          pass
 66  
 67      def greet(self):
 68          """
 69          Greets the user.
 70          """
 71          return "Hello"
 72  
 73  
 74  @deprecated(since="1.0.0")
 75  @dataclass
 76  class DeprecatedDataClass:
 77      """
 78      A deprecated dataclass.
 79      """
 80  
 81      x: int
 82      y: int
 83  
 84      def add(self):
 85          return self.x + self.y
 86  
 87  
 88  @deprecated(since="1.0.0")
 89  @dataclass
 90  class AnotherDeprecatedDataClass:
 91      a: int
 92      b: int
 93  
 94      def add(self):
 95          return self.a + self.b
 96  
 97  
 98  @deprecated()
 99  @dataclass
100  class AnotherDeprecatedDataClassOrder:
101      """
102      A deprecated dataclass with decorators in different order.
103      """
104  
105      m: int
106      n: int
107  
108  
109  def test_deprecated_method():
110      msg = "``tests.utils.test_annotations.MyClass.method`` is deprecated"
111      with pytest.warns(FutureWarning, match=re.escape(msg)):
112          assert MyClass().method() == 0
113      docstring = MyClass.method.__doc__
114      assert docstring is not None
115      assert msg in docstring
116  
117  
118  def test_deprecated_function():
119      msg = "``tests.utils.test_annotations.function`` is deprecated"
120      with pytest.warns(FutureWarning, match=re.escape(msg)):
121          assert function() == 1
122      docstring = function.__doc__
123      assert docstring is not None
124      assert msg in docstring
125  
126  
127  def test_empty_docstring():
128      docstring = ""
129      expected_indent = ""
130      assert _get_min_indent_of_docstring(docstring) == expected_indent
131  
132  
133  def test_single_line_docstring():
134      docstring = """Single line with indent."""
135      expected_indent = ""
136      assert _get_min_indent_of_docstring(docstring) == expected_indent
137  
138  
139  def test_multi_line_docstring_first_line():
140      first_line_docstring = """Description
141  
142      Args:
143          x: x
144  
145      Returns:
146          y
147      """
148      expected_indent = "    "
149      assert _get_min_indent_of_docstring(first_line_docstring) == expected_indent
150  
151  
152  def test_multi_line_docstring_second_line():
153      second_line_docstring = """
154      Description
155  
156      Args:
157          x: x
158  
159      Returns:
160          y
161      """
162      expected_indent = "    "
163      assert _get_min_indent_of_docstring(second_line_docstring) == expected_indent
164  
165  
166  def test_deprecated_and_keyword_first():
167      docstring = deprecated_and_keyword_only_first.__doc__
168      assert docstring is not None
169      assert docstring.rstrip() == (
170          """    .. note:: This method requires all argument be specified by keyword.
171      .. Warning:: ``tests.utils.test_annotations.deprecated_and_keyword_only_first`` is deprecated since 0.0.0. This method will be removed in a future release.
172  Description
173  
174      Args:
175          x: x
176  
177      Returns:
178          y"""  # noqa: E501
179      )
180  
181  
182  def test_deprecated_and_keyword_second():
183      docstring = deprecated_and_keyword_only_second.__doc__
184      assert docstring is not None
185      assert docstring.rstrip() == (
186          """    .. Warning:: ``tests.utils.test_annotations.deprecated_and_keyword_only_second`` is deprecated since 0.0.0. This method will be removed in a future release.
187      .. note:: This method requires all argument be specified by keyword.
188  
189      Description
190  
191      Args:
192          x: x
193  
194      Returns:
195          y"""  # noqa: E501
196      )
197  
198  
199  def test_deprecated_class():
200      msg = "``tests.utils.test_annotations.DeprecatedClass`` is deprecated"
201      with pytest.warns(FutureWarning, match=re.escape(msg)):
202          DeprecatedClass()
203      docstring = DeprecatedClass.__doc__
204      assert docstring is not None
205      assert msg in docstring
206  
207  
208  def test_deprecated_class_method():
209      msg = "``tests.utils.test_annotations.DeprecatedClass`` is deprecated"
210      with pytest.warns(FutureWarning, match=re.escape(msg)):
211          instance = DeprecatedClass()
212      assert instance.greet() == "Hello"
213      docstring = DeprecatedClass.__doc__
214      assert docstring is not None
215      assert msg in docstring
216  
217  
218  def test_deprecated_dataclass():
219      msg = "``tests.utils.test_annotations.DeprecatedDataClass`` is deprecated since 1.0.0"
220      with pytest.warns(FutureWarning, match=re.escape(msg)):
221          DeprecatedDataClass(x=10, y=20)
222      docstring = DeprecatedDataClass.__doc__
223      assert docstring is not None
224      assert msg in docstring
225  
226  
227  def test_deprecated_dataclass_fields():
228      msg = "``tests.utils.test_annotations.DeprecatedDataClass`` is deprecated since 1.0.0"
229      with pytest.warns(FutureWarning, match=re.escape(msg)):
230          instance = DeprecatedDataClass(x=5, y=15)
231      assert instance.x == 5
232      assert instance.y == 15
233      docstring = DeprecatedDataClass.__doc__
234      assert docstring is not None
235      assert msg in docstring
236  
237  
238  def test_deprecated_dataclass_method():
239      msg = "``tests.utils.test_annotations.AnotherDeprecatedDataClass`` is deprecated since 1.0.0"
240      with pytest.warns(FutureWarning, match=re.escape(msg)):
241          instance = AnotherDeprecatedDataClass(a=3, b=4)
242      assert instance.add() == 7
243      docstring = AnotherDeprecatedDataClass.__doc__
244      assert docstring is not None
245      assert msg in docstring
246  
247  
248  def test_deprecated_dataclass_different_order():
249      msg = "``tests.utils.test_annotations.AnotherDeprecatedDataClassOrder`` is deprecated"
250      with pytest.warns(FutureWarning, match=re.escape(msg)):
251          AnotherDeprecatedDataClassOrder(m=7, n=8)
252      docstring = AnotherDeprecatedDataClassOrder.__doc__
253      assert docstring is not None
254      assert msg in docstring
255  
256  
257  def test_deprecated_dataclass_dunder_methods():
258      instance = DeprecatedDataClass(x=1, y=2)
259  
260      assert instance.x == 1
261      assert instance.y == 2
262  
263      expected_repr = "DeprecatedDataClass(x=1, y=2)"
264      assert repr(instance) == expected_repr
265  
266      instance2 = DeprecatedDataClass(x=1, y=2)
267      instance3 = DeprecatedDataClass(x=2, y=3)
268      assert instance == instance2
269      assert instance != instance3
270  
271  
272  def test_deprecated_dataclass_preserves_fields():
273      instance = DeprecatedDataClass(x=100, y=200)
274      field_names = {f.name for f in fields(DeprecatedDataClass)}
275      assert field_names == {"x", "y"}
276      assert instance.x == 100
277      assert instance.y == 200
278  
279  
280  def test_deprecated_dataclass_preserves_methods():
281      instance = DeprecatedDataClass(x=10, y=20)
282      assert instance.add() == 30
283  
284  
285  def test_deprecated_dataclass_preserves_class_attributes():
286      assert DeprecatedDataClass.__module__ == "tests.utils.test_annotations"
287      assert DeprecatedDataClass.__qualname__ == "DeprecatedDataClass"
288  
289  
290  def test_deprecated_dataclass_dunder_methods_not_mutated():
291      instance = DeprecatedDataClass(x=5, y=10)
292      assert instance.x == 5
293      assert instance.y == 10
294  
295      expected_repr = "DeprecatedDataClass(x=5, y=10)"
296      assert repr(instance) == expected_repr
297  
298      same_instance = DeprecatedDataClass(x=5, y=10)
299      different_instance = DeprecatedDataClass(x=1, y=2)
300      assert instance == same_instance
301      assert instance != different_instance
302  
303      assert instance.add() == 15
304  
305      allowed_attrs = {"x", "y", "add"}
306      attrs = {attr for attr in dir(instance) if not attr.startswith("__")}
307      assert attrs == allowed_attrs
308  
309  
310  def test_deprecated_dataclass_special_methods_integrity():
311      instance = DeprecatedDataClass(x=42, y=84)
312  
313      assert instance.x == 42
314      assert instance.y == 84
315  
316      expected_repr = "DeprecatedDataClass(x=42, y=84)"
317      assert repr(instance) == expected_repr
318  
319      same_instance = DeprecatedDataClass(x=42, y=84)
320      different_instance = DeprecatedDataClass(x=1, y=2)
321      assert instance == same_instance
322      assert instance != different_instance
323  
324      assert instance.add() == 126
325  
326      allowed_attrs = {"x", "y", "add"}
327      attrs = {attr for attr in dir(instance) if not attr.startswith("__")}
328      assert attrs == allowed_attrs