pyproteum 0.1__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.
pyproteum/__init__.py ADDED
@@ -0,0 +1 @@
1
+
pyproteum/__main__.py ADDED
@@ -0,0 +1,22 @@
1
+ from pyproteum import testnew, tcase, mutagen, exemuta, mutaview
2
+ import sys
3
+
4
+ if __name__ == '__main__':
5
+ del sys.argv[0]
6
+ if sys.argv ==[]:
7
+ print('You should use one of the pyproteum commands.')
8
+ else:
9
+ match sys.argv[0]:
10
+ case 'testnew':
11
+ testnew.main()
12
+ case 'tcase':
13
+ tcase.main()
14
+ case 'mutagen':
15
+ mutagen.main()
16
+ case 'exemuta':
17
+ exemuta.main()
18
+ case 'mutaview':
19
+ mutaview.main()
20
+ case _:
21
+ print(f'Not found statement {sys.argv[0]}')
22
+
pyproteum/exemuta.py ADDED
@@ -0,0 +1,316 @@
1
+ import sys, csv
2
+ import os,io
3
+ from pyproteum.models.models import *
4
+ import unittest
5
+ import pickle
6
+ import types
7
+ from pyproteum.moperators.myoperator import *
8
+ from pyproteum.tcase import change_dir_connect
9
+ from pyproteum.tcase import load_module, get_all_test_names
10
+ import timeout_decorator
11
+ import inspect
12
+
13
+
14
+ class RegistrandoResultado(unittest.TextTestResult):
15
+ def __init__(self, stream, descriptions, verbosity, number, research):
16
+ super().__init__(stream, descriptions, verbosity)
17
+ self.my_successes = [] # armazena os testes que passaram
18
+ self.timeouts = []
19
+ self.all = [] # todos os testes executados
20
+ self.all_order = {}
21
+ self.order = number
22
+ self.count = 0
23
+ self.research = research
24
+
25
+ def startTest(self, test):
26
+ self.all.append(test)
27
+ self.count += 1
28
+ self.all_order[test.id()] = '{}:{}'.format(self.order, self.count)
29
+ super().startTest(test)
30
+
31
+ def addSuccess(self, test):
32
+ self.my_successes.append(test)
33
+ super().addSuccess(test)
34
+
35
+ def stopTest(self, test):
36
+ super().stopTest(test)
37
+
38
+ def addFailure(self, test, err):
39
+ etype, evalue, tb = err
40
+ # Marcação explícita de TIMEOUT
41
+ if getattr(etype, "__name__", "") in ("TimeoutError", "TimeoutError_", "TimeoutException"):
42
+ # opcional: anotar em um campo extra
43
+ self.timeouts.append(test)
44
+ super().addFailure(test, err)
45
+ if not self.research:
46
+ self.stop()
47
+
48
+ def addError(self, test, err):
49
+ etype, evalue, tb = err
50
+ # Marcação explícita de TIMEOUT
51
+ if getattr(etype, "__name__", "") in ("TimeoutError", "TimeoutError_", "TimeoutException"):
52
+ # opcional: anotar em um campo extra
53
+ self.timeouts.append(test)
54
+ super().addError(test, err)
55
+ if not self.research:
56
+ self.stop()
57
+
58
+
59
+ class RegistrandoRunner(unittest.TextTestRunner):
60
+
61
+ def __init__(self, *args, number=0, research=False, **kwargs):
62
+ super().__init__(*args, **kwargs)
63
+ self.number = number
64
+ self.research = research
65
+
66
+ def _makeResult(self):
67
+ return RegistrandoResultado(self.stream, self.descriptions, self.verbosity, self.number,self.research)
68
+
69
+
70
+
71
+
72
+ def create_module_from_ast(nome_modulo, arvore_ast):
73
+ modulo = types.ModuleType(nome_modulo)
74
+ code = compile(arvore_ast, filename="<ast>", mode="exec")
75
+ exec(code, modulo.__dict__)
76
+ sys.modules[nome_modulo] = modulo
77
+ return modulo
78
+
79
+
80
+
81
+
82
+ def __exec():
83
+ session_name = sys.argv[-1]
84
+ directory = None
85
+ i = 2
86
+ keep = False
87
+ while i < len(sys.argv[:-1]):
88
+ s = sys.argv[i]
89
+ match s:
90
+ case '--D':
91
+ i += 1
92
+ directory = sys.argv[i]
93
+ case '--keep':
94
+ keep = True
95
+ case _:
96
+ usage()
97
+ return
98
+ i += 1
99
+
100
+ change_dir_connect(directory, session_name)
101
+ session = Session.get(Session.id==1)
102
+ rse = session.type == 'research'
103
+
104
+ fields = get_all_test_names()
105
+ Execution = create_exec_db(fields)
106
+
107
+ cont_dead = 0
108
+ cont_live = 0
109
+ cont_equiv = 0
110
+
111
+ for muta in Mutant:
112
+ if muta.status == 'Equivalent':
113
+ cont_equiv += 1
114
+ continue
115
+
116
+ if keep and muta.status != 'live' and not rse:
117
+ cont_dead += 1
118
+ continue
119
+ print('Mutant {} -- {}'.format(muta.id, muta.operator))
120
+ tree = pickle.loads(muta.ast)
121
+ create_module_from_ast(os.path.splitext(os.path.basename(muta.source.filename))[0], tree)
122
+ reg_muta = Execution.create(mutant=muta)
123
+ #print(reg_muta)
124
+ dead = False
125
+ for testfile in TestCase:
126
+ try:
127
+ module_test = load_module(testfile.filename)
128
+ aplicar_timeout_em_tests(module_test)
129
+ suite = unittest.TestLoader().loadTestsFromModule(module_test)
130
+ resultado = RegistrandoRunner(stream=io.StringIO(), research=rse,verbosity=0,number=testfile.id).run(suite)
131
+ except Exception as ex:
132
+ print(f'Error. Can not run test file {testfile}')
133
+ print(ex)
134
+ sys.exit()
135
+
136
+ for test,_ in resultado.failures:
137
+ t = resultado.all_order[test.id()]
138
+ if test in resultado.timeouts:
139
+ s = 'timeout'
140
+ else:
141
+ s = 'fail'
142
+ print(f'{t} {s}.')
143
+
144
+ setattr(reg_muta, test.id(), s)
145
+ reg_muta.save()
146
+ dead = True
147
+
148
+ for test in resultado.my_successes:
149
+ t = resultado.all_order[test.id()]
150
+ print(f'{t} passed.')
151
+ setattr(reg_muta, test.id(), 'live')
152
+ reg_muta.save()
153
+ for test,_ in resultado.errors:
154
+ t = resultado.all_order[test.id()]
155
+ print(f'{t} error.')
156
+ setattr(reg_muta, test.id(), 'error')
157
+ reg_muta.save()
158
+ dead = True
159
+ if dead and not rse:
160
+ break
161
+
162
+
163
+ if dead:
164
+ muta.status = 'Dead'
165
+ print('Dead')
166
+ cont_dead += 1
167
+ muta.save()
168
+ else:
169
+ cont_live += 1
170
+
171
+ print(f'Alive: {cont_live}')
172
+ print(f'Dead: {cont_dead}')
173
+ print(f'Equivalent: {cont_equiv}')
174
+ print('Mutation score: {:.2f}'.format(cont_dead/(cont_live+cont_dead)))
175
+
176
+
177
+ def create_exec_db(fields):
178
+ model_dict = { 'mutant' : ForeignKeyField(Mutant, backref='executed')}
179
+ fields = sorted(fields)
180
+ for r in fields:
181
+ model_dict[r] = TextField(default='No Exec')
182
+ Execution = criar_modelo('Execution', model_dict)
183
+ db.drop_tables([Execution])
184
+ db.create_tables([Execution])
185
+ return Execution
186
+
187
+ TIMEOUT_SEGUNDOS = 2
188
+
189
+ def aplicar_timeout_em_tests(modulo):
190
+ for nome, obj in inspect.getmembers(modulo):
191
+ if inspect.isclass(obj) and issubclass(obj, unittest.TestCase):
192
+ for metodo_nome, metodo in inspect.getmembers(obj, inspect.isfunction):
193
+ if metodo_nome.startswith("test_"):
194
+ setattr(obj, metodo_nome, timeout_decorator.timeout(TIMEOUT_SEGUNDOS, use_signals=False)(metodo))
195
+
196
+ def __csv():
197
+ session_name = sys.argv.pop()
198
+ directory = None
199
+ outname = None
200
+ i = 2
201
+ while i < len(sys.argv[:-1]):
202
+ s = sys.argv[i]
203
+ match s:
204
+ case '--D':
205
+ i += 1
206
+ directory = sys.argv[i]
207
+ case '--O':
208
+ i += 1
209
+ outname = sys.argv[i]
210
+ case _:
211
+ usage()
212
+ return
213
+ i += 1
214
+
215
+ change_dir_connect(directory, session_name)
216
+
217
+ if outname is None:
218
+ outname = session_name+'.csv'
219
+
220
+ try:
221
+ Execution = get_table_model('Execution')
222
+ except Exception as ex:
223
+ print('Previous execution not found. Try "exemuta.py --exec" before exporting')
224
+ print(ex)
225
+ sys.exit()
226
+
227
+ try:
228
+ with open(outname, 'w') as out:
229
+ writer = csv.writer(out)
230
+ campos = [field.name for field in Execution._meta.sorted_fields]
231
+ campos.remove('id')
232
+ writer.writerow(campos)
233
+ for registro in Execution.select():
234
+ writer.writerow([getattr(registro, campo) for campo in campos])
235
+ print(f'{outname} successfully generated')
236
+ except Exception as ex:
237
+ print(f'Could not generate {outname}')
238
+ print(ex)
239
+ sys.exit()
240
+
241
+
242
+ def __equiv():
243
+ session_name = sys.argv[-1]
244
+ directory = None
245
+ muta_number = None
246
+ i = 2
247
+ while i < len(sys.argv[:-2]):
248
+ s = sys.argv[i]
249
+ match s:
250
+ case '--D':
251
+ i += 1
252
+ directory = sys.argv[i]
253
+ case '--x':
254
+ i += 1
255
+ list_number = sys.argv[i]
256
+ case _:
257
+ usage()
258
+ return
259
+ i += 1
260
+
261
+ if not list_number:
262
+ print('Mutant number not provided')
263
+ sys.exit()
264
+
265
+ try:
266
+ change_dir_connect(directory, session_name)
267
+ except Exception as ex:
268
+ print(f'Can not access test session')
269
+ print(ex)
270
+ sys.exit()
271
+
272
+ for muta_number in list_number.split():
273
+ try:
274
+ muta_number = int(muta_number)
275
+ reg_muta = Mutant.get(Mutant.id==muta_number)
276
+ print('Mutant: ', reg_muta.id)
277
+ if not reg_muta.status in ['live','equiv']:
278
+ print(f'Warning: mutant {reg_muta.id} is not alive. It is {reg_muta.status}')
279
+ reg_muta.status = 'equiv'
280
+ reg_muta.save()
281
+ except Exception as ex:
282
+ print(f'Can not access mutant number {muta_number}')
283
+ print(ex)
284
+ #sys.exit()
285
+
286
+ def main():
287
+ n = len(sys.argv)-2
288
+ if n < 1:
289
+ usage()
290
+
291
+ if sys.argv[1] == '--exec':
292
+ __exec()
293
+ return
294
+ elif sys.argv[1] == '--csv':
295
+ __csv()
296
+ return
297
+ elif sys.argv[1] == '--equiv':
298
+ __equiv()
299
+ return
300
+ else:
301
+ usage()
302
+
303
+
304
+ def usage():
305
+ print('Usage:')
306
+ print('exemuta --exec [--keep] [--D <directory> ] <session name>')
307
+ print('\tExecute the mutants with the test cases in the session')
308
+ print('\t--keep: execute only the live mutants')
309
+ print('exemuta --csv [--D <directory> ] <session name>')
310
+ print('\tExports the last execution to a csv file')
311
+ print('exemuta --equiv --x <list of numbers> [--D <directory> ] <session name>')
312
+ print('\tMarks mutants as equivalents')
313
+ sys.exit()
314
+
315
+ if __name__ == '__main__' :
316
+ main()
@@ -0,0 +1,89 @@
1
+ from peewee import *
2
+ from datetime import datetime
3
+
4
+ db = Proxy()
5
+
6
+ class BaseModel(Model):
7
+ class Meta:
8
+ database = db
9
+
10
+ class Session(BaseModel):
11
+ filename = CharField(unique=True)
12
+ type = TextField()
13
+ #ast = BlobField()
14
+ criado = DateTimeField(default=datetime.now)
15
+
16
+ class TestCase(BaseModel):
17
+ filename = CharField(unique=True)
18
+ criado = DateTimeField(default=datetime.now)
19
+
20
+
21
+ class Mutant(BaseModel):
22
+ source = ForeignKeyField(Session, backref='mutants')
23
+ operator = TextField()
24
+ function = TextField()
25
+ func_lineno = IntegerField()
26
+ func_end_lineno = IntegerField()
27
+ lineno = IntegerField()
28
+ col_offset = IntegerField()
29
+ end_lineno = IntegerField()
30
+ end_col_offset = IntegerField()
31
+ seq_number = IntegerField()
32
+ ast = BlobField()
33
+ status = TextField(default='live')
34
+ criado = DateTimeField(default=datetime.now)
35
+
36
+ class Meta:
37
+ indexes = (
38
+ (('source', 'lineno', 'col_offset','operator','seq_number'), True), # combinação única
39
+ )
40
+
41
+ def __str__(self):
42
+ s = f'Source: {self.source.filename}\n'
43
+ s += f'Operator: {self.operator}\n'
44
+ s += f'Func Lineno: {self.func_lineno}\n'
45
+ s += f'Func End Lineno: {self.func_end_lineno}\n'
46
+ s += f'Lineno: {self.lineno}\n'
47
+ s += f'End Lineno: {self.end_lineno}\n'
48
+ return s
49
+
50
+ def criar_modelo(nome_modelo, campos):
51
+ class Meta:
52
+ database = db
53
+
54
+ atributos = dict(campos)
55
+ atributos['Meta'] = Meta
56
+ return type(nome_modelo, (Model,), atributos)
57
+
58
+ #uso:
59
+ # campos = {
60
+ # 'titulo': CharField(),
61
+ # 'ano': IntegerField(),
62
+ # 'autor': CharField(),
63
+ # }
64
+
65
+ # Livro = criar_modelo('Livro', campos, db)
66
+ # db.create_tables([Livro])
67
+ # Livro.create(titulo='Dom Casmurro', ano=1899, autor='Machado de Assis')
68
+ #db.drop_tables([Model1, Model2])
69
+
70
+ def get_table_model(table_name):
71
+ colunas = db.get_columns(table_name)
72
+ atributos = {
73
+ '_meta': type('Meta', (), {'database': db, 'table_name': table_name})
74
+ }
75
+ for col in colunas:
76
+ atributos[col.name] = Field()
77
+ return type(table_name, (BaseModel,), atributos)
78
+
79
+ # Livro = get_table_model('Livro')
80
+
81
+ def criar_modelo_dinamico(nome_tabela):
82
+ colunas = db.get_columns(nome_tabela)
83
+ atributos = {
84
+ '_meta': type('Meta', (), {'database': db, 'table_name': nome_tabela})
85
+ }
86
+ for col in colunas:
87
+ # Usa Field genérico para não errar o tipo
88
+ atributos[col.name] = Field()
89
+ return type(nome_tabela, (BaseModel,), atributos)
@@ -0,0 +1,34 @@
1
+ import ast
2
+ from pyproteum.moperators.myoperator import *
3
+
4
+
5
+ class Cccr(MyOperator):
6
+
7
+ def __init__(self, original):
8
+ super().__init__(original)
9
+ self.seq = 1
10
+ self.const_set = set()
11
+
12
+ def visit_FunctionDef(self, node):
13
+ old_set = set(self.const_set)
14
+ super().visit_FunctionDef(node)
15
+ self.const_set = old_set
16
+ return node
17
+
18
+ def visit_Constant(self, node):
19
+ old_value = node.value
20
+ self.const_set.add(old_value)
21
+
22
+ for new_value in self.const_set:
23
+ if new_value == old_value:
24
+ continue
25
+ if type(new_value) != type(old_value):
26
+ continue
27
+ node.value = new_value
28
+ self.salva_muta(node, self.function, self.func_lineno, self.func_end_lineno,seq=self.seq)
29
+ self.seq += 1
30
+
31
+ node.value = old_value
32
+ self.generic_visit(node)
33
+ return node
34
+
@@ -0,0 +1,56 @@
1
+ import ast
2
+ from pyproteum.moperators.myoperator import *
3
+ import copy
4
+
5
+
6
+ class Ccsr(MyOperator):
7
+
8
+ def __init__(self, original):
9
+ super().__init__(original)
10
+ self.seq = 1
11
+ self.scalar_set = set()
12
+ ParentSetter().visit(self.original)
13
+
14
+
15
+
16
+ def visit_FunctionDef(self, node):
17
+ old_set = set(self.scalar_set)
18
+ super().visit_FunctionDef(node)
19
+ self.scalar_set = old_set
20
+ return node
21
+
22
+ def visit_Name(self, node):
23
+ if isinstance(node.ctx, ast.Store) :
24
+ self.scalar_set.add(ast.Name(id=copy.deepcopy(node.id), ctx=ast.Load()))
25
+ self.generic_visit(node)
26
+ return node
27
+
28
+ def visit_arg(self, node):
29
+ if node.arg != 'self':
30
+ self.scalar_set.add(ast.Name(id=copy.deepcopy(node.arg), ctx=ast.Load()))
31
+ self.generic_visit(node)
32
+ return node
33
+
34
+ def visit_Constant(self, node):
35
+ p, fld, idx = node.parent, node.parent_field, node.parent_index
36
+
37
+
38
+ for new_value in self.scalar_set:
39
+ ast.copy_location(new_value, node)
40
+ if idx is not None:
41
+ lst = getattr(p, fld)
42
+ lst[idx] = new_value
43
+ else:
44
+ setattr(p,fld, new_value)
45
+ self.salva_muta(p, self.function, self.func_lineno, self.func_end_lineno,seq=self.seq)
46
+ self.seq += 1
47
+
48
+ if idx is not None:
49
+ lst = getattr(p, fld)
50
+ lst[idx] = node
51
+ else:
52
+ setattr(p,fld, node)
53
+
54
+ self.generic_visit(node)
55
+ return node
56
+
@@ -0,0 +1,33 @@
1
+
2
+ import ast
3
+ from pyproteum.moperators.myoperator import *
4
+
5
+
6
+ class Crcr(MyOperator):
7
+
8
+ def __init__(self, original):
9
+ super().__init__(original)
10
+ self.seq = 1
11
+
12
+ def visit_Constant(self, node):
13
+ old_value = node.value
14
+
15
+ for new_value in self.get_required(node.value):
16
+ if new_value == old_value:
17
+ continue
18
+ node.value = new_value
19
+ self.salva_muta(node, self.function, self.func_lineno, self.func_end_lineno,seq=self.seq)
20
+ self.seq += 1
21
+ node.value = old_value
22
+ return self.generic_visit(node)
23
+
24
+ def get_required(self, value):
25
+ if isinstance(value, int):
26
+ return [-1, 0, 1]
27
+ if isinstance(value, float):
28
+ return [-1.0, 0.0, 1.0, 1E-13, 1E+13]
29
+ if isinstance(value, str):
30
+ return ['',' ']
31
+ if isinstance(value, bytes):
32
+ return [b'',b' ']
33
+ return []
@@ -0,0 +1,155 @@
1
+ import ast
2
+ import copy
3
+ from pyproteum.print_visit import PrintVisit
4
+
5
+
6
+
7
+ class MyOperator(ast.NodeTransformer):
8
+
9
+ def __init__(self, original):
10
+ self.original = copy.deepcopy(original)
11
+ self.mutants = []
12
+ self.function = ''
13
+ self.func_lineno = 0
14
+ self.func_end_lineno = 0
15
+ self.docstrings = set()
16
+ self.pre_visit(self.original)
17
+
18
+ def pre_visit(self, tree):
19
+ for node in ast.walk(tree):
20
+ try:
21
+ if ast.get_docstring(node):
22
+ fn = node.body[0]
23
+ self.docstrings.add(id(fn))
24
+ except:
25
+ continue
26
+
27
+ def salva_muta(self, node, func, ln, eln, seq=1):
28
+ cop = copy.deepcopy(self.original)
29
+ #self.ajustar_col_offsets(cop, node.lineno, node.col_offset, desloca)
30
+ ast.fix_missing_locations(cop)
31
+
32
+ if self.check_sintaxe(cop):
33
+ muta = MutantDict({'function': func,
34
+ 'func_lineno': ln,
35
+ 'func_end_lineno': eln,
36
+ 'operator': str(self),
37
+ 'lineno':node.lineno,
38
+ 'col_offset':node.col_offset,
39
+ 'end_lineno':node.end_lineno,
40
+ 'end_col_offset':node.end_col_offset,
41
+ 'seq_number' : seq,
42
+ 'ast':cop
43
+ })
44
+ self.mutants.append(muta)
45
+ else:
46
+ print(f'Skiping mutant {str(self)}')
47
+ pass
48
+
49
+
50
+
51
+ # def ajustar_col_offsets(self, node_raiz, linha_alvo, col_alvo, deslocamento):
52
+ # for no in ast.walk(node_raiz):
53
+ # if hasattr(no, 'lineno') and hasattr(no, 'col_offset'):
54
+ # if no.lineno == linha_alvo and no.col_offset > col_alvo:
55
+ # no.col_offset += deslocamento
56
+ # # print(no, linha_alvo, col_alvo, no.lineno, no.col_offset)
57
+
58
+
59
+ def go_visit(self):
60
+ self.visit(self.original)
61
+
62
+ def visit_FunctionDef(self, node):
63
+ old_f = self.function
64
+ old_l = self.func_lineno
65
+ old_el = self.func_end_lineno
66
+ self.function = node.name
67
+ self.func_lineno = node.lineno
68
+ self.func_end_lineno = node.end_lineno
69
+ r = self.generic_visit(node)
70
+ self.function = old_f
71
+ self.func_end_lineno = old_el
72
+ self.func_lineno = old_l
73
+ return r
74
+
75
+ def visit_Expr(self, node):
76
+ if id(node) in self.docstrings:
77
+ return node
78
+ return self.generic_visit(node)
79
+
80
+ def __str__(self):
81
+ s = str(type(self))
82
+ return s [-6:-2].lower()
83
+
84
+
85
+ def check_sintaxe(self,tree):
86
+ unv = PrintVisit(tree)
87
+ try:
88
+ unv.go_visit()
89
+ ast.parse(str(unv))
90
+ return True
91
+ except Exception as ex:
92
+ print(unv)
93
+ print(ex)
94
+ return False
95
+
96
+
97
+ def show_node(self, node):
98
+ print('\n', node)
99
+ if hasattr(node, 'lineno'):
100
+ print(f'lineno: {node.lineno}')
101
+ print(f'end_lineno: {node.end_lineno}')
102
+ print(f'col_offset: {node.col_offset}')
103
+ print(f'end_col_offset: {node.end_col_offset}')
104
+
105
+ class MutantDict(dict):
106
+
107
+
108
+ def __lt__(self, other):
109
+ if self['operator'] < other['operator']:
110
+ return True
111
+ if self['operator'] > other['operator']:
112
+ return False
113
+ if self['lineno'] < other['lineno']:
114
+ return True
115
+ if self['lineno'] > other['lineno']:
116
+ return False
117
+ if self['col_offset'] < other['col_offset']:
118
+ return True
119
+ if self['col_offset'] > other['col_offset']:
120
+ return False
121
+ if self['seq_number'] < other['seq_number']:
122
+ return True
123
+ if self['seq_number'] > other['seq_number']:
124
+ return False
125
+ return False
126
+
127
+
128
+ # This class is used to set the parent of each node.
129
+ # Not all operators use this
130
+ # CCsr
131
+ class ParentSetter(ast.NodeVisitor):
132
+ def visit(self, node):
133
+ # garante que todo nó tenha attrs (pai/campo/índice podem já ter sido definidos)
134
+ if not hasattr(node, "parent"):
135
+ node.parent = None
136
+ node.parent_field = None
137
+ node.parent_index = None
138
+ super().visit(node)
139
+
140
+ def generic_visit(self, node):
141
+ for field, value in ast.iter_fields(node):
142
+ if isinstance(value, ast.AST):
143
+ value.parent = node
144
+ value.parent_field = field
145
+ value.parent_index = None
146
+ self.visit(value)
147
+ elif isinstance(value, list):
148
+ for i, item in enumerate(value):
149
+ if isinstance(item, ast.AST):
150
+ item.parent = node
151
+ item.parent_field = field
152
+ item.parent_index = i
153
+ self.visit(item)
154
+
155
+