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