/ tests / dev / test_check_function_signatures.py
test_check_function_signatures.py
  1  import ast
  2  
  3  from dev.check_function_signatures import check_signature_compatibility
  4  
  5  
  6  def test_no_changes():
  7      old_code = "def func(a, b=1): pass"
  8      new_code = "def func(a, b=1): pass"
  9  
 10      old_tree = ast.parse(old_code)
 11      new_tree = ast.parse(new_code)
 12      errors = check_signature_compatibility(old_tree.body[0], new_tree.body[0])
 13  
 14      assert len(errors) == 0
 15  
 16  
 17  def test_positional_param_removed():
 18      old_code = "def func(a, b, c): pass"
 19      new_code = "def func(a, b): pass"
 20  
 21      old_tree = ast.parse(old_code)
 22      new_tree = ast.parse(new_code)
 23      errors = check_signature_compatibility(old_tree.body[0], new_tree.body[0])
 24  
 25      assert len(errors) == 1
 26      assert errors[0].message == "Positional param 'c' was removed."
 27      assert errors[0].param_name == "c"
 28  
 29  
 30  def test_positional_param_renamed():
 31      old_code = "def func(a, b): pass"
 32      new_code = "def func(x, b): pass"
 33  
 34      old_tree = ast.parse(old_code)
 35      new_tree = ast.parse(new_code)
 36      errors = check_signature_compatibility(old_tree.body[0], new_tree.body[0])
 37  
 38      assert len(errors) == 1
 39      assert "Positional param order/name changed: 'a' -> 'x'." in errors[0].message
 40      assert errors[0].param_name == "x"
 41  
 42  
 43  def test_only_first_positional_rename_flagged():
 44      old_code = "def func(a, b, c, d): pass"
 45      new_code = "def func(x, y, z, w): pass"
 46  
 47      old_tree = ast.parse(old_code)
 48      new_tree = ast.parse(new_code)
 49      errors = check_signature_compatibility(old_tree.body[0], new_tree.body[0])
 50  
 51      assert len(errors) == 1
 52      assert "Positional param order/name changed: 'a' -> 'x'." in errors[0].message
 53  
 54  
 55  def test_optional_positional_became_required():
 56      old_code = "def func(a, b=1): pass"
 57      new_code = "def func(a, b): pass"
 58  
 59      old_tree = ast.parse(old_code)
 60      new_tree = ast.parse(new_code)
 61      errors = check_signature_compatibility(old_tree.body[0], new_tree.body[0])
 62  
 63      assert len(errors) == 1
 64      assert errors[0].message == "Optional positional param 'b' became required."
 65      assert errors[0].param_name == "b"
 66  
 67  
 68  def test_multiple_optional_became_required():
 69      old_code = "def func(a, b=1, c=2): pass"
 70      new_code = "def func(a, b, c): pass"
 71  
 72      old_tree = ast.parse(old_code)
 73      new_tree = ast.parse(new_code)
 74      errors = check_signature_compatibility(old_tree.body[0], new_tree.body[0])
 75  
 76      assert len(errors) == 2
 77      assert errors[0].message == "Optional positional param 'b' became required."
 78      assert errors[1].message == "Optional positional param 'c' became required."
 79  
 80  
 81  def test_new_required_positional_param():
 82      old_code = "def func(a): pass"
 83      new_code = "def func(a, b): pass"
 84  
 85      old_tree = ast.parse(old_code)
 86      new_tree = ast.parse(new_code)
 87      errors = check_signature_compatibility(old_tree.body[0], new_tree.body[0])
 88  
 89      assert len(errors) == 1
 90      assert errors[0].message == "New required positional param 'b' added."
 91      assert errors[0].param_name == "b"
 92  
 93  
 94  def test_new_optional_positional_param_allowed():
 95      old_code = "def func(a): pass"
 96      new_code = "def func(a, b=1): pass"
 97  
 98      old_tree = ast.parse(old_code)
 99      new_tree = ast.parse(new_code)
100      errors = check_signature_compatibility(old_tree.body[0], new_tree.body[0])
101  
102      assert len(errors) == 0
103  
104  
105  def test_keyword_only_param_removed():
106      old_code = "def func(*, a, b): pass"
107      new_code = "def func(*, b): pass"
108  
109      old_tree = ast.parse(old_code)
110      new_tree = ast.parse(new_code)
111      errors = check_signature_compatibility(old_tree.body[0], new_tree.body[0])
112  
113      assert len(errors) == 1
114      assert errors[0].message == "Keyword-only param 'a' was removed."
115      assert errors[0].param_name == "a"
116  
117  
118  def test_multiple_keyword_only_removed():
119      old_code = "def func(*, a, b, c): pass"
120      new_code = "def func(*, b): pass"
121  
122      old_tree = ast.parse(old_code)
123      new_tree = ast.parse(new_code)
124      errors = check_signature_compatibility(old_tree.body[0], new_tree.body[0])
125  
126      assert len(errors) == 2
127      error_messages = {e.message for e in errors}
128      assert "Keyword-only param 'a' was removed." in error_messages
129      assert "Keyword-only param 'c' was removed." in error_messages
130  
131  
132  def test_optional_keyword_only_became_required():
133      old_code = "def func(*, a=1): pass"
134      new_code = "def func(*, a): pass"
135  
136      old_tree = ast.parse(old_code)
137      new_tree = ast.parse(new_code)
138      errors = check_signature_compatibility(old_tree.body[0], new_tree.body[0])
139  
140      assert len(errors) == 1
141      assert errors[0].message == "Keyword-only param 'a' became required."
142      assert errors[0].param_name == "a"
143  
144  
145  def test_new_required_keyword_only_param():
146      old_code = "def func(*, a): pass"
147      new_code = "def func(*, a, b): pass"
148  
149      old_tree = ast.parse(old_code)
150      new_tree = ast.parse(new_code)
151      errors = check_signature_compatibility(old_tree.body[0], new_tree.body[0])
152  
153      assert len(errors) == 1
154      assert errors[0].message == "New required keyword-only param 'b' added."
155      assert errors[0].param_name == "b"
156  
157  
158  def test_new_optional_keyword_only_allowed():
159      old_code = "def func(*, a): pass"
160      new_code = "def func(*, a, b=1): pass"
161  
162      old_tree = ast.parse(old_code)
163      new_tree = ast.parse(new_code)
164      errors = check_signature_compatibility(old_tree.body[0], new_tree.body[0])
165  
166      assert len(errors) == 0
167  
168  
169  def test_complex_mixed_violations():
170      old_code = "def func(a, b=1, *, c, d=2): pass"
171      new_code = "def func(x, b, *, c=3, e): pass"
172  
173      old_tree = ast.parse(old_code)
174      new_tree = ast.parse(new_code)
175      errors = check_signature_compatibility(old_tree.body[0], new_tree.body[0])
176  
177      assert len(errors) == 3
178      error_messages = [e.message for e in errors]
179      assert any("Positional param order/name changed: 'a' -> 'x'." in msg for msg in error_messages)
180      assert any("Keyword-only param 'd' was removed." in msg for msg in error_messages)
181      assert any("New required keyword-only param 'e' added." in msg for msg in error_messages)
182  
183  
184  def test_parameter_error_has_location_info():
185      old_code = "def func(a): pass"
186      new_code = "def func(b): pass"
187  
188      old_tree = ast.parse(old_code)
189      new_tree = ast.parse(new_code)
190      errors = check_signature_compatibility(old_tree.body[0], new_tree.body[0])
191  
192      assert len(errors) == 1
193      assert errors[0].lineno == 1
194      assert errors[0].col_offset > 0
195  
196  
197  def test_async_function_compatibility():
198      old_code = "async def func(a, b=1): pass"
199      new_code = "async def func(a, b): pass"
200  
201      old_tree = ast.parse(old_code)
202      new_tree = ast.parse(new_code)
203      errors = check_signature_compatibility(old_tree.body[0], new_tree.body[0])
204  
205      assert len(errors) == 1
206      assert errors[0].message == "Optional positional param 'b' became required."
207  
208  
209  def test_positional_only_compatibility():
210      old_code = "def func(a, /): pass"
211      new_code = "def func(b, /): pass"
212  
213      old_tree = ast.parse(old_code)
214      new_tree = ast.parse(new_code)
215      errors = check_signature_compatibility(old_tree.body[0], new_tree.body[0])
216  
217      assert len(errors) == 1
218      assert "Positional param order/name changed: 'a' -> 'b'." in errors[0].message
219  
220  
221  def test_rename_stops_further_positional_checks():
222      old_code = "def func(a, b=1, c=2): pass"
223      new_code = "def func(x, b, c): pass"
224  
225      old_tree = ast.parse(old_code)
226      new_tree = ast.parse(new_code)
227      errors = check_signature_compatibility(old_tree.body[0], new_tree.body[0])
228  
229      assert len(errors) == 1
230      assert "Positional param order/name changed: 'a' -> 'x'." in errors[0].message