skylos 2.0.0__py3-none-any.whl → 2.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of skylos might be problematic. Click here for more details.

skylos/visitor.py CHANGED
@@ -1,11 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  import ast
3
3
  from pathlib import Path
4
- import os
5
- import re, logging, traceback
6
- DBG = bool(int(os.getenv("SKYLOS_DEBUG", "0")))
7
- log = logging.getLogger("Skylos")
8
-
4
+ import re
9
5
 
10
6
  PYTHON_BUILTINS={"print", "len", "str", "int", "float", "list", "dict", "set", "tuple", "range", "open",
11
7
  "super", "object", "type", "enumerate", "zip", "map", "filter", "sorted", "sum", "min",
@@ -61,6 +57,9 @@ class Visitor(ast.NodeVisitor):
61
57
  self.exports= set()
62
58
  self.current_function_scope= []
63
59
  self.current_function_params= []
60
+ self.local_var_maps = []
61
+ self.in_cst_class = 0
62
+ self.local_type_maps = []
64
63
 
65
64
  def add_def(self, name, t, line):
66
65
  if name not in {d.name for d in self.defs}:
@@ -89,17 +88,10 @@ class Visitor(ast.NodeVisitor):
89
88
  if not isinstance(annotation_str, str):
90
89
  return
91
90
 
92
- if DBG:
93
- log.debug(f"parsing annotation: {annotation_str}")
94
-
95
-
96
91
  try:
97
92
  parsed = ast.parse(annotation_str, mode="eval")
98
93
  self.visit(parsed.body)
99
94
  except:
100
- if DBG:
101
- if DBG: log.debug("annotation parse failed")
102
-
103
95
  for tok in re.findall(r"[A-Za-z_][A-Za-z0-9_]*", annotation_str):
104
96
  self.add_ref(tok)
105
97
 
@@ -159,7 +151,9 @@ class Visitor(ast.NodeVisitor):
159
151
  self.add_def(qualified_name,"method"if self.cls else"function",node.lineno)
160
152
 
161
153
  self.current_function_scope.append(node.name)
162
-
154
+ self.local_var_maps.append({})
155
+ self.local_type_maps.append({})
156
+
163
157
  old_params = self.current_function_params
164
158
  self.current_function_params = []
165
159
 
@@ -179,6 +173,8 @@ class Visitor(ast.NodeVisitor):
179
173
 
180
174
  self.current_function_scope.pop()
181
175
  self.current_function_params = old_params
176
+ self.local_var_maps.pop()
177
+ self.local_type_maps.pop()
182
178
 
183
179
  visit_AsyncFunctionDef= visit_FunctionDef
184
180
 
@@ -186,29 +182,86 @@ class Visitor(ast.NodeVisitor):
186
182
  cname =f"{self.mod}.{node.name}"
187
183
  self.add_def(cname, "class",node.lineno)
188
184
 
185
+ is_cst = False
186
+
189
187
  for base in node.bases:
188
+ base_name = ""
189
+ if isinstance(base, ast.Attribute):
190
+ base_name = base.attr
191
+ elif isinstance(base, ast.Name):
192
+ base_name = base.id
190
193
  self.visit(base)
194
+ if base_name in {"CSTTransformer", "CSTVisitor"}:
195
+ is_cst = True
191
196
  for keyword in node.keywords:
192
197
  self.visit(keyword.value)
193
198
  for decorator in node.decorator_list:
194
199
  self.visit(decorator)
195
200
 
196
201
  prev= self.cls
202
+ if is_cst:
203
+ self.in_cst_class += 1
197
204
 
198
205
  self.cls= node.name
199
206
  for b in node.body:
200
207
  self.visit(b)
201
208
 
202
209
  self.cls= prev
210
+ if is_cst:
211
+ self.in_cst_class -= 1
203
212
 
204
213
  def visit_AnnAssign(self, node):
205
214
  self.visit_annotation(node.annotation)
206
215
  if node.value:
207
216
  self.visit(node.value)
208
- self.visit(node.target)
217
+
218
+ def _define(t):
219
+ if isinstance(t, ast.Name):
220
+ name_simple = t.id
221
+ scope_parts = [self.mod]
222
+ if self.cls: scope_parts.append(self.cls)
223
+ if self.current_function_scope: scope_parts.extend(self.current_function_scope)
224
+ prefix = '.'.join(filter(None, scope_parts))
225
+ if prefix:
226
+ var_name = f"{prefix}.{name_simple}"
227
+ else:
228
+ var_name = name_simple
229
+
230
+ self.add_def(var_name, "variable", t.lineno)
231
+ if self.current_function_scope and self.local_var_maps:
232
+ self.local_var_maps[-1][name_simple] = var_name
233
+ elif isinstance(t, (ast.Tuple, ast.List)):
234
+ for elt in t.elts:
235
+ _define(elt)
236
+ _define(node.target)
209
237
 
210
238
  def visit_AugAssign(self, node):
211
- self.visit(node.target)
239
+ if isinstance(node.target, ast.Name):
240
+ nm = node.target.id
241
+ if (self.current_function_scope and
242
+ self.local_var_maps and
243
+ self.local_var_maps and
244
+ nm in self.local_var_maps[-1]):
245
+
246
+ self.add_ref(self.local_var_maps[-1][nm])
247
+
248
+ else:
249
+ self.add_ref(self.qual(nm))
250
+ scope_parts = [self.mod]
251
+ if self.cls: scope_parts.append(self.cls)
252
+
253
+ if self.current_function_scope: scope_parts.extend(self.current_function_scope)
254
+ prefix = '.'.join(filter(None, scope_parts))
255
+ if prefix:
256
+ var_name = f"{prefix}.{nm}"
257
+ else:
258
+ var_name = nm
259
+
260
+ self.add_def(var_name, "variable", node.lineno)
261
+ if self.current_function_scope and self.local_var_maps:
262
+ self.local_var_maps[-1][nm] = var_name
263
+ else:
264
+ self.visit(node.target)
212
265
  self.visit(node.value)
213
266
 
214
267
  def visit_Subscript(self, node):
@@ -227,21 +280,27 @@ class Visitor(ast.NodeVisitor):
227
280
  def process_target_for_def(target_node):
228
281
  if isinstance(target_node, ast.Name):
229
282
  name_simple = target_node.id
283
+ if (name_simple == "METADATA_DEPENDENCIES"
284
+ and self.cls and self.in_cst_class > 0):
285
+ return
230
286
  if name_simple == "__all__" and not self.current_function_scope and not self.cls:
231
287
  return
232
288
 
233
289
  scope_parts = [self.mod]
234
-
235
290
  if self.cls:
236
291
  scope_parts.append(self.cls)
237
-
238
292
  if self.current_function_scope:
239
293
  scope_parts.extend(self.current_function_scope)
240
-
294
+
241
295
  prefix = '.'.join(filter(None, scope_parts))
242
- var_name = f"{prefix}.{name_simple}" if prefix else name_simple
296
+ if prefix:
297
+ var_name = f"{prefix}.{name_simple}"
298
+ else:
299
+ var_name = name_simple
243
300
 
244
301
  self.add_def(var_name, "variable", target_node.lineno)
302
+ if self.current_function_scope and self.local_var_maps:
303
+ self.local_var_maps[-1][name_simple] = var_name
245
304
 
246
305
  elif isinstance(target_node, (ast.Tuple, ast.List)):
247
306
  for elt in target_node.elts:
@@ -249,7 +308,7 @@ class Visitor(ast.NodeVisitor):
249
308
 
250
309
  for t in node.targets:
251
310
  process_target_for_def(t)
252
-
311
+
253
312
  for target in node.targets:
254
313
  if isinstance(target, ast.Name) and target.id == "__all__":
255
314
  if isinstance(node.value, (ast.List, ast.Tuple)):
@@ -259,12 +318,48 @@ class Visitor(ast.NodeVisitor):
259
318
  value = elt.value
260
319
  elif hasattr(elt, 's') and isinstance(elt.s, str):
261
320
  value = elt.s
262
-
321
+
263
322
  if value is not None:
264
- export_name = f"{self.mod}.{value}" if self.mod else value
323
+ if self.mod:
324
+ export_name = f"{self.mod}.{value}"
325
+ else:
326
+ export_name = value
327
+
265
328
  self.add_ref(export_name)
266
- self.add_ref(value)
267
-
329
+ self.add_ref(value)
330
+
331
+ try:
332
+ if isinstance(node.value, ast.Call):
333
+ callee = node.value.func
334
+ fqname = None
335
+
336
+ if isinstance(callee, ast.Name):
337
+ fqname = self.alias.get(callee.id, self.qual(callee.id))
338
+
339
+ elif isinstance(callee, ast.Attribute):
340
+ parts = []
341
+ cur = callee
342
+ while isinstance(cur, ast.Attribute):
343
+ parts.append(cur.attr)
344
+ cur = cur.value
345
+ head = None
346
+ if isinstance(cur, ast.Name):
347
+ head = self.alias.get(cur.id, self.qual(cur.id))
348
+ if head:
349
+ fqname = ".".join([head] + list(reversed(parts)))
350
+
351
+ if fqname and self.current_function_scope and self.local_type_maps:
352
+ def _mark_target(t):
353
+ if isinstance(t, ast.Name):
354
+ self.local_type_maps[-1][t.id] = fqname
355
+ elif isinstance(t, (ast.Tuple, ast.List)):
356
+ for elt in t.elts:
357
+ _mark_target(elt)
358
+ for t in node.targets:
359
+ _mark_target(t)
360
+ except Exception:
361
+ pass
362
+
268
363
  self.generic_visit(node)
269
364
 
270
365
  def visit_Call(self, node):
@@ -284,7 +379,10 @@ class Visitor(ast.NodeVisitor):
284
379
  elif isinstance(node.args[0], ast.Name):
285
380
  target_name = node.args[0].id
286
381
  if target_name != "self":
287
- self.dyn.add(self.mod.split(".")[0] if self.mod else "")
382
+ if self.mod:
383
+ self.dyn.add(self.mod.split(".")[0])
384
+ else:
385
+ self.dyn.add("")
288
386
 
289
387
  elif isinstance(node.func, ast.Name) and node.func.id == "globals":
290
388
  parent = getattr(node, 'parent', None)
@@ -296,33 +394,80 @@ class Visitor(ast.NodeVisitor):
296
394
  self.add_ref(f"{self.mod}.{func_name}")
297
395
 
298
396
  elif isinstance(node.func, ast.Name) and node.func.id in ("eval", "exec"):
299
- self.dyn.add(self.mod.split(".")[0] if self.mod else "")
397
+ root_mod = ""
398
+ if self.mod:
399
+ root_mod = self.mod.split(".")[0]
400
+ self.dyn.add(root_mod)
300
401
 
301
402
  def visit_Name(self, node):
302
403
  if isinstance(node.ctx, ast.Load):
303
404
  for param_name, param_full_name in self.current_function_params:
304
405
  if node.id == param_name:
305
406
  self.add_ref(param_full_name)
306
- break
407
+ return
408
+
409
+ if (self.current_function_scope and
410
+ self.local_var_maps
411
+ and self.local_var_maps
412
+ and node.id in self.local_var_maps[-1]):
307
413
 
308
- else:
309
- # not parameter, handle normally
310
- qualified = self.qual(node.id)
311
- self.add_ref(qualified)
312
- if node.id in DYNAMIC_PATTERNS:
313
- self.dyn.add(self.mod.split(".")[0])
414
+ self.add_ref(self.local_var_maps[-1][node.id])
415
+ return
416
+
417
+ qualified = self.qual(node.id)
418
+ self.add_ref(qualified)
419
+ if node.id in DYNAMIC_PATTERNS:
420
+ self.dyn.add(self.mod.split(".")[0])
314
421
 
315
422
  def visit_Attribute(self, node):
316
423
  self.generic_visit(node)
317
- if isinstance(node.ctx, ast.Load) and isinstance(node.value, ast.Name):
318
- if node.value.id in [param_name for param_name, _ in self.current_function_params]:
319
- # mark parameter as referenced
320
- for param_name, param_full_name in self.current_function_params:
321
- if node.value.id == param_name:
322
- self.add_ref(param_full_name)
323
- break
324
-
325
- self.add_ref(f"{self.qual(node.value.id)}.{node.attr}")
424
+
425
+ if not isinstance(node.ctx, ast.Load):
426
+ return
427
+
428
+ if isinstance(node.value, ast.Name):
429
+ base = node.value.id
430
+
431
+ param_hit = None
432
+ for param_name, param_full in self.current_function_params:
433
+ if base == param_name:
434
+ param_hit = (param_name, param_full)
435
+ break
436
+
437
+ if param_hit:
438
+ self.add_ref(param_hit[1])
439
+
440
+ if self.cls and base in {"self", "cls"}:
441
+ if self.mod:
442
+ owner = f"{self.mod}.{self.cls}"
443
+ else:
444
+ owner = self.cls
445
+
446
+ self.add_ref(f"{owner}.{node.attr}")
447
+ return
448
+
449
+ if (self.current_function_scope and
450
+ self.local_type_maps and
451
+ self.local_type_maps[-1].get(base)):
452
+
453
+ self.add_ref(f"{self.local_type_maps[-1][base]}.{node.attr}")
454
+ return
455
+
456
+ self.add_ref(f"{self.qual(base)}.{node.attr}")
457
+
458
+ def visit_NamedExpr(self, node):
459
+ self.visit(node.value)
460
+ if isinstance(node.target, ast.Name):
461
+ nm = node.target.id
462
+ scope_parts = [self.mod]
463
+ if self.cls: scope_parts.append(self.cls)
464
+ if self.current_function_scope: scope_parts.extend(self.current_function_scope)
465
+ prefix = '.'.join(filter(None, scope_parts))
466
+ var_name = f"{prefix}.{nm}" if prefix else nm
467
+ self.add_def(var_name, "variable", node.lineno)
468
+ if self.current_function_scope and self.local_var_maps:
469
+ self.local_var_maps[-1][nm] = var_name
470
+ self.add_ref(var_name)
326
471
 
327
472
  def visit_keyword(self, node):
328
473
  self.visit(node.value)
@@ -1,8 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: skylos
3
- Version: 2.0.0
3
+ Version: 2.1.0
4
4
  Summary: A static analysis tool for Python codebases
5
5
  Author-email: oha <aaronoh2015@gmail.com>
6
6
  Requires-Python: >=3.9
7
7
  Requires-Dist: inquirer>=3.0.0
8
+ Requires-Dist: flask>=2.1.0
9
+ Requires-Dist: flask-cors>=3.0.0
10
+ Requires-Dist: libcst>=1.8.2
8
11
  Dynamic: requires-python
@@ -1,11 +1,12 @@
1
- skylos/__init__.py,sha256=xIvJTzzvvgfcQWYMckJFXNNlXPpU1gjO1IldPy7T_N4,151
2
- skylos/analyzer.py,sha256=TDr_szli65cuQWD6BUcQCUFleM1R-QZD0uRUoHlU6As,14145
3
- skylos/cli.py,sha256=Xqc_vnueic0D39FI0YzMtJObLsn8MqFFCUnY1nrpEt4,18115
1
+ skylos/__init__.py,sha256=sr_HNrjL2O7hbyh_8vN2tG1ZKw8sXJVegKjp3G8Iz9c,151
2
+ skylos/analyzer.py,sha256=JoQ_otvLmI5k64y3h11jVZEWuA6u3npY15Nwy1vrGi0,14995
3
+ skylos/cli.py,sha256=bRxhvKkw_VWQVHT6WxKyX9i3qJjGuJJaEJ3QI8FIpZw,15887
4
+ skylos/codemods.py,sha256=A5dNwTJiYtgP3Mv8NQ03etdfi9qNHSECv1GRpLyDCYU,3213
4
5
  skylos/constants.py,sha256=vAfKls0OuR5idtXzeMDfYv5-wLtYWDe1NW5TSUJT8RI,1636
5
- skylos/framework_aware.py,sha256=vbZ1CZIL_2TOEkdH8uVSPPihLM2ctqA01zEHL8HG5sw,5768
6
+ skylos/framework_aware.py,sha256=eX4oU0jQwDWejkkW4kjRNctre27sVLHK1CTDDiqPqRw,13054
6
7
  skylos/server.py,sha256=5Rlgy3LdE8I5TWRJJh0n19JqhVYaAOc9fUtRjL-PpX8,16270
7
8
  skylos/test_aware.py,sha256=kxYoMG2m02kbMlxtFOM-MWJO8qqxHviP9HgAMbKRfvU,2304
8
- skylos/visitor.py,sha256=FXNck0qjVB3lfp4X63D5-bE1NsvUTbfvT17ivIevhjk,12654
9
+ skylos/visitor.py,sha256=i-YWVenc8mG0dWzYi945HmktIE2RkgL7rBT7VUnhkLI,18073
9
10
  test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
11
  test/compare_tools.py,sha256=0g9PDeJlbst-7hOaQzrL4MiJFQKpqM8q8VeBGzpPczg,22738
11
12
  test/conftest.py,sha256=57sTF6vLL5U0CVKwGQFJcRs6n7t1dEHIriQoSluNmAI,6418
@@ -13,8 +14,9 @@ test/diagnostics.py,sha256=ExuFOCVpc9BDwNYapU96vj9RXLqxji32Sv6wVF4nJYU,13802
13
14
  test/test_analyzer.py,sha256=vZ-0cSF_otm7ZYdp6U3a8eWGwRZ6Tf0tVuJxYqcxdqM,22816
14
15
  test/test_changes_analyzer.py,sha256=l1hspCFz-sF8gqOilJvntUDuGckwhYsVtzvSRB13ZWw,5085
15
16
  test/test_cli.py,sha256=rtdKzanDRJT_F92jKkCQFdhvlfwVJxfXKO8Hrbn-mIg,13180
17
+ test/test_codemods.py,sha256=Tbp9jE95HDl77EenTmyTtB1Sc3L8fwY9xiMNVDN5lxo,4522
16
18
  test/test_constants.py,sha256=pMuDy0UpC81zENMDCeK6Bqmm3BR_HHZQSlMG-9TgOm0,12602
17
- test/test_framework_aware.py,sha256=tJ7bnhiGeSdsAvrWaGJO5ovTrw9n9BBk6o1HCw15yDA,11693
19
+ test/test_framework_aware.py,sha256=G9va1dEQ31wsvd4X7ROf_1YhhAQG5CogB7v0hYCojQ8,8802
18
20
  test/test_integration.py,sha256=bNKGUe-w0xEZEdnoQNHbssvKMGs9u9fmFQTOz1lX9_k,12398
19
21
  test/test_skylos.py,sha256=kz77STrS4k3Eez5RDYwGxOg2WH3e7zNZPUYEaTLbGTs,15608
20
22
  test/test_test_aware.py,sha256=VmbR_MQY0m941CAxxux8OxJHIr7l8crfWRouSeBMhIo,9390
@@ -26,8 +28,8 @@ test/sample_repo/sample_repo/commands.py,sha256=b6gQ9YDabt2yyfqGbOpLo0osF7wya8O4
26
28
  test/sample_repo/sample_repo/models.py,sha256=xXIg3pToEZwKuUCmKX2vTlCF_VeFA0yZlvlBVPIy5Qw,3320
27
29
  test/sample_repo/sample_repo/routes.py,sha256=8yITrt55BwS01G7nWdESdx8LuxmReqop1zrGUKPeLi8,2475
28
30
  test/sample_repo/sample_repo/utils.py,sha256=S56hEYh8wkzwsD260MvQcmUFOkw2EjFU27nMLFE6G2k,1103
29
- skylos-2.0.0.dist-info/METADATA,sha256=gVlJTPqEjfbbnn0TGbDT5aEx8TBtD1r-mitvOfJBI-Y,224
30
- skylos-2.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
31
- skylos-2.0.0.dist-info/entry_points.txt,sha256=zzRpN2ByznlQoLeuLolS_TFNYSQxUGBL1EXQsAd6bIA,43
32
- skylos-2.0.0.dist-info/top_level.txt,sha256=f8GA_7KwfaEopPMP8-EXDQXaqd4IbsOQPakZy01LkdQ,12
33
- skylos-2.0.0.dist-info/RECORD,,
31
+ skylos-2.1.0.dist-info/METADATA,sha256=oE2audt2wkmDLNdDcZCHgmDxtrZY8gygMnDwI5Qi6V4,314
32
+ skylos-2.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
33
+ skylos-2.1.0.dist-info/entry_points.txt,sha256=zzRpN2ByznlQoLeuLolS_TFNYSQxUGBL1EXQsAd6bIA,43
34
+ skylos-2.1.0.dist-info/top_level.txt,sha256=f8GA_7KwfaEopPMP8-EXDQXaqd4IbsOQPakZy01LkdQ,12
35
+ skylos-2.1.0.dist-info/RECORD,,
test/test_codemods.py ADDED
@@ -0,0 +1,153 @@
1
+ import textwrap
2
+ from skylos.codemods import (
3
+ remove_unused_import_cst,
4
+ remove_unused_function_cst,
5
+ )
6
+
7
+ def _line_no(code: str, startswith: str) -> int:
8
+ for i, line in enumerate(code.splitlines(), start=1):
9
+ if line.lstrip().startswith(startswith):
10
+ return i
11
+ raise AssertionError(f"Line starting with {startswith!r} not found")
12
+
13
+ def test_remove_simple_import_entire_line():
14
+ code = "import os\nprint(1)\n"
15
+ ln = _line_no(code, "import os")
16
+ new, changed = remove_unused_import_cst(code, "os", ln)
17
+ assert changed is True
18
+ assert "import os" not in new
19
+ assert "print(1)" in new
20
+
21
+ def test_remove_one_name_from_multi_import():
22
+ code = "import os, sys\n"
23
+ ln = _line_no(code, "import os, sys")
24
+ new, changed = remove_unused_import_cst(code, "os", ln)
25
+ assert changed is True
26
+ assert new.strip() == "import sys"
27
+
28
+ def test_remove_from_import_keeps_other_names():
29
+ code = "from a import b, c\n"
30
+ ln = _line_no(code, "from a import")
31
+ new, changed = remove_unused_import_cst(code, "b", ln)
32
+ assert changed is True
33
+ assert new.strip() == "from a import c"
34
+
35
+ def test_remove_from_import_with_alias_uses_bound_name():
36
+ code = "from a import b as c, d\n"
37
+ ln = _line_no(code, "from a import")
38
+ new, changed = remove_unused_import_cst(code, "c", ln)
39
+ assert changed is True
40
+ assert new.strip() == "from a import d"
41
+
42
+ def test_parenthesized_multiline_from_import_preserves_formatting():
43
+ code = textwrap.dedent(
44
+ """\
45
+ from x.y import (
46
+ a,
47
+ b, # keep me
48
+ c,
49
+ )
50
+ use = b
51
+ """
52
+ )
53
+ ln = _line_no(code, "from x.y import")
54
+ new, changed = remove_unused_import_cst(code, "a", ln)
55
+ assert changed is True
56
+ assert "a," not in new
57
+ assert "b, # keep me" in new
58
+ assert "c," in new
59
+
60
+ def test_import_star_is_noop():
61
+ code = "from x import *\n"
62
+ ln = _line_no(code, "from x import *")
63
+ new, changed = remove_unused_import_cst(code, "*", ln)
64
+ assert changed is False
65
+ assert new == code
66
+
67
+ def test_dotted_import_requires_bound_leftmost_segment():
68
+ code = "import pkg.sub\n"
69
+ ln = _line_no(code, "import pkg.sub")
70
+ new, changed = remove_unused_import_cst(code, "pkg", ln)
71
+ assert changed is True
72
+ assert "import pkg.sub" not in new
73
+
74
+ new2, changed2 = remove_unused_import_cst(code, "sub", ln)
75
+ assert changed2 is False
76
+ assert new2 == code
77
+
78
+ def test_import_idempotency():
79
+ code = "import os, sys\n"
80
+ ln = _line_no(code, "import os, sys")
81
+ new, changed = remove_unused_import_cst(code, "os", ln)
82
+ assert changed is True
83
+ new2, changed2 = remove_unused_import_cst(new, "os", ln)
84
+ assert changed2 is False
85
+ assert new2 == new
86
+
87
+ def test_remove_simple_function_block():
88
+ code = textwrap.dedent(
89
+ """\
90
+ def unused():
91
+ x = 1
92
+ return x
93
+
94
+ def used():
95
+ return 42
96
+ """
97
+ )
98
+ ln = _line_no(code, "def unused")
99
+ new, changed = remove_unused_function_cst(code, "unused", ln)
100
+ assert changed is True
101
+ assert "def unused" not in new
102
+ assert "def used" in new
103
+
104
+ def test_remove_decorated_function_removes_decorators_too():
105
+ code = textwrap.dedent(
106
+ """\
107
+ @dec1
108
+ @dec2(arg=1)
109
+ def target():
110
+ return 1
111
+
112
+ def other():
113
+ return 2
114
+ """
115
+ )
116
+ ln = _line_no(code, "def target")
117
+ new, changed = remove_unused_function_cst(code, "target", ln)
118
+ assert changed is True
119
+ assert "@dec1" not in new and "@dec2" not in new
120
+ assert "def target" not in new
121
+ assert "def other" in new
122
+
123
+ def test_remove_async_function():
124
+ code = textwrap.dedent(
125
+ """\
126
+ async def coro():
127
+ return 1
128
+
129
+ def ok():
130
+ return 2
131
+ """
132
+ )
133
+ ln = _line_no(code, "async def coro")
134
+ new, changed = remove_unused_function_cst(code, "coro", ln)
135
+ assert changed is True
136
+ assert "async def coro" not in new
137
+ assert "def ok" in new
138
+
139
+ def test_function_wrong_line_noop():
140
+ code = "def f():\n return 1\n"
141
+ ln = 999
142
+ new, changed = remove_unused_function_cst(code, "f", ln)
143
+ assert changed is False
144
+ assert new == code
145
+
146
+ def test_function_idempotency():
147
+ code = "def g():\n return 1\n"
148
+ ln = _line_no(code, "def g")
149
+ new, changed = remove_unused_function_cst(code, "g", ln)
150
+ assert changed is True
151
+ new2, changed2 = remove_unused_function_cst(new, "g", ln)
152
+ assert changed2 is False
153
+ assert new2 == new