-
Notifications
You must be signed in to change notification settings - Fork 46
/
Copy pathreplace_attrs.py
572 lines (528 loc) · 32.3 KB
/
replace_attrs.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import re
from pathlib import Path
from lxml import etree
NEW_ATTRS = ['invisible', 'required', 'readonly', 'column_invisible']
def get_files_recursive(path):
return (str(p) for p in Path(path).glob('**/*.xml') if p.is_file())
root_dir = input('Enter root directory to check (empty for current directory) : ')
root_dir = root_dir or '.'
all_xml_files = get_files_recursive(root_dir)
def normalize_domain(domain):
"""
Normalize Domain, taken from odoo/osv/expression.py -> just the part so that & operators are added where needed.
After that, we can use a part of the def parse() from the same file to manage parenthesis for and/or
:rtype: list[str|tuple]
"""
if len(domain) == 1:
return domain
result = []
expected = 1 # expected number of expressions
op_arity = {'!': 1, '&': 2, '|': 2}
for token in domain:
if expected == 0: # more than expected, like in [A, B]
result[0:0] = ['&'] # put an extra '&' in front
expected = 1
if isinstance(token, (list, tuple)): # domain term
expected -= 1
token = tuple(token)
else:
expected += op_arity.get(token, 0) - 1
result.append(token)
return result
def stringify_leaf(leaf):
"""
:param tuple leaf:
:rtype: str
"""
stringify = ''
switcher = False
case_insensitive = False
# Replace operators not supported in python (=, like, ilike)
operator = str(leaf[1])
# Take left operand, never to add quotes (should be python object / field)
left_operand = leaf[0]
# Take care of right operand, don't add quotes if it's list/tuple/set/boolean/number, check if we have a true/false/1/0 string tho.
right_operand = leaf[2]
# Handle '=?'
if operator == '=?':
if type(right_operand) is str:
right_operand = f"'{right_operand}'"
return f"({right_operand} in [None, False] or {left_operand} == {right_operand})"
# Handle '='
elif operator == '=':
if right_operand in (False, []): # Check for False or empty list
return f"not {left_operand}"
elif right_operand == True: # Check for True using '==' comparison so only boolean values can evaluate to True
return left_operand
operator = '=='
# Handle '!='
elif operator == '!=':
if right_operand in (False, []): # Check for False or empty list
return left_operand
elif right_operand == True: # Check for True using '==' comparison so only boolean values can evaluate to True
return f"not {left_operand}"
# Handle 'like' and other operators
elif 'like' in operator:
case_insensitive = 'ilike' in operator
if type(right_operand) is str and re.search('[_%]', right_operand):
# Since wildcards won't work/be recognized after conversion we throw an error so we don't end up with
# expressions that behave differently from their originals
raise Exception("Script doesn't support 'like' domains with wildcards")
if operator in ['=like', '=ilike']:
operator = '=='
else:
if 'not' in operator:
operator = 'not in'
else:
operator = 'in'
switcher = True
if type(right_operand) is str:
right_operand = f"'{right_operand}'"
if switcher:
temp_operand = left_operand
left_operand = right_operand
right_operand = temp_operand
if not case_insensitive:
stringify = f"{left_operand} {operator} {right_operand}"
else:
stringify = f"{left_operand}.lower() {operator} {right_operand}.lower()"
return stringify
def stringify_attr(stack):
"""
:param bool|str|int|list stack:
:rtype: str
"""
if stack in (True, False, 'True', 'False', 1, 0, '1', '0'):
return str(stack)
last_parenthesis_index = max(index for index, item in enumerate(stack[::-1]) if item not in ('|', '!'))
stack = normalize_domain(stack)
stack = stack[::-1]
result = []
for index, leaf_or_operator in enumerate(stack):
if leaf_or_operator == '!':
expr = result.pop()
result.append('(not (%s))' % expr)
elif leaf_or_operator in ['&', '|']:
left = result.pop()
# In case of a single | or single & , we expect that it's a tag that have an attribute AND a state
# the state will be added as OR in states management
try:
right = result.pop()
except IndexError:
res = left + ('%s' % ' and' if leaf_or_operator == '&' else ' or')
result.append(res)
continue
form = '(%s %s %s)'
if index > last_parenthesis_index:
form = '%s %s %s'
result.append(form % (left, 'and' if leaf_or_operator == '&' else 'or', right))
else:
result.append(stringify_leaf(leaf_or_operator))
result = result[0]
return result
def get_new_attrs(attrs):
"""
:param str attrs:
:rtype: dict[bool|str|int]
"""
new_attrs = {}
# Temporarily replace dynamic variables (field reference, context value, %()d) in leafs by strings prefixed with '__dynamic_variable__.'
# This way the evaluation won't fail on these strings and we can later identify them to convert back to their original values
escaped_operators = ['=', '!=', '>', '>=', '<', '<=', '=\?', '=like', 'like', 'not like', 'ilike', 'not ilike', '=ilike', 'in', 'not in', 'child_of', 'parent_of']
attrs = re.sub("<", "<", attrs)
attrs = re.sub(">", ">", attrs)
attrs = re.sub(f"([\"'](?:{'|'.join(escaped_operators)})[\"']\s*,\s*)(?!False|True)([\w\.]+)(?=\s*[\]\)])", r"\1'__dynamic_variable__.\2'", attrs)
attrs = re.sub(r"(%\([\w\.]+\)d)", r"'__dynamic_variable__.\1'", attrs)
attrs = attrs.strip()
if re.search("^{.*}$", attrs, re.DOTALL):
# attrs can be an empty value, in which case the eval() would fail, so only eval attrs representing dictionaries
attrs_dict = eval(attrs.strip())
for attr, attr_value in attrs_dict.items():
if attr not in NEW_ATTRS:
# We don't know what to do with attributes not in NEW_ATTR, so the user will have to process those
# manually when checking the differences post-conversion
continue
stringified_attr = stringify_attr(attr_value)
if type(stringified_attr) is str:
# Convert dynamic variable strings back to their original form
stringified_attr = re.sub(r"'__dynamic_variable__\.([^']+)'", r"\1", stringified_attr)
new_attrs[attr] = stringified_attr
return new_attrs
autoreplace = input('Do you want to auto-replace attributes ? (y/n) (empty == no) (will not ask confirmation for each file) : ') or 'n'
nofilesfound = True
ok_files = []
nok_files = []
def get_parent_etree_node(root_node, target_node):
"""
Returns the parent node of a given node, and the index and indentation of the target node in the parent node's direct child nodes list
:param xml.etree.ElementTree.Element root_node:
:param xml.etree.ElementTree.Element target_node:
:returns: index, parent_node, indentation
:rtype: (int, xml.etree.ElementTree.Element, str)
"""
for parent_elem in root_node.iter():
previous_child = False
for i, child in enumerate(list(parent_elem)):
if child == target_node:
if previous_child:
indent = previous_child.tail
else:
# For the first child element it's the text in between the parent's opening tag and the first child that determines indentation
indent = parent_elem.text
return i, parent_elem, indent
previous_child = child
def get_child_tag_at_index(parent_node, index):
"""
Returns the child node of a node with a given index
:param xml.etree.ElementTree.Element parent_node:
:param int index:
:returns: child_node
:rtype: xml.etree.ElementTree.Element
"""
for i, child in enumerate(list(parent_node)):
if i == index:
return child
def get_sibling_attribute_tag_of_type(root_node, target_node, attribute_name):
"""
If it exists, returns the attribute tag with the same parent tag for the given name
:param xml.etree.ElementTree.Element root_node:
:param xml.etree.ElementTree.Element target_node:
:param str attribute_name:
:returns: attribute_tag with name="<attribute_name>"
:rtype: xml.etree.ElementTree.Element
"""
_, xpath_node, _ = get_parent_etree_node(root_node, target_node)
if node := xpath_node.xpath(f"./attribute[@name='{attribute_name}']"):
return node[0]
def get_inherited_tag_type(root_node, target_node):
"""
Checks what the type of the tag is that the attribute tag applies to
:param xml.etree.ElementTree.Element root_node:
:param xml.etree.ElementTree.Element target_node:
:rtype: str|None
"""
_, parent_tag, _ = get_parent_etree_node(root_node, target_node)
if expr := parent_tag.get('expr'):
# Checks if the last part of the xpath expression is a tag name and returns it
# If not (eg. if the pattern is for example expr="//field[@name='...']/.."), return None
if matches := re.findall("^.*/(\w+)[^/]*?$", expr):
return matches[0]
else:
return parent_tag.tag
def get_combined_invisible_condition(invisible_attribute, states_attribute):
"""
:param str invisible_attribute: invisible attribute condition already present on the same tag as the states
:param str states_attribute: string of the form 'state1,state2,...'
"""
invisible_attribute = invisible_attribute.strip()
states_attribute = states_attribute.strip()
if not states_attribute:
return invisible_attribute
states_list = re.split(r"\s*,\s*", states_attribute.strip())
states_to_add = f"state not in {states_list}"
if invisible_attribute:
if invisible_attribute.endswith('or') or invisible_attribute.endswith('and'):
combined_invisible_condition = f"{invisible_attribute} {states_to_add}"
else:
combined_invisible_condition = f"{invisible_attribute} or {states_to_add}"
else:
combined_invisible_condition = states_to_add
return combined_invisible_condition
for xml_file in all_xml_files:
try:
with open(xml_file, 'rb') as f:
contents = f.read().decode('utf-8')
f.close()
if not 'attrs' in contents and not 'states' in contents:
continue
convert_line_separator_back_to_windows = False
if '\r\n' in contents:
# The ElementTree parser parses line separators as '\n', so to ensure we don't change the line separator
# when updating the file we should convert the '\n' back to '\r\n' after serializing the ElementTree
convert_line_separator_back_to_windows = True
# etree can't parse xml strings with an encoding declaration, so first we strip this from the file content
# we'll then re-add the declaration once we convert the ElementTree back to its string representation
has_encoding_declaration = False
if encoding_declaration := re.search(r"\A.*<\?xml.*?encoding=.*?\?>\s*", contents, re.DOTALL):
has_encoding_declaration = True
contents = re.sub(r"\A.*<\?xml.*?encoding=.*?\?>\s*", "", contents, re.DOTALL)
# Parse the document int an ElementTree
doc = etree.fromstring(contents)
tags_with_attrs = doc.xpath("//*[@attrs]")
attribute_tags_with_attrs = doc.xpath("//attribute[@name='attrs']")
tags_with_states = doc.xpath("//*[@states]")
attribute_tags_with_states = doc.xpath("//attribute[@name='states']")
if not (tags_with_attrs or attribute_tags_with_attrs or tags_with_states or attribute_tags_with_states):
continue
print('\n#############################' + ((6 + len(xml_file)) * '#'))
print('##### Taking care of file -> %s' % xml_file)
print('\n##### Current tags found #####\n')
for t in tags_with_attrs + attribute_tags_with_attrs + tags_with_states + attribute_tags_with_states:
print(etree.tostring(t, encoding='unicode'))
nofilesfound = False
# Management of tags that have attrs=""
for tag in tags_with_attrs:
all_attributes = []
# TODO: combine existing and new invisible, required, readonly and column_invisible attributes
# If both an attrs and one of these attributes are present at the same time, if the attribute is True
# then it overrides the domain in the attrs dict. If it is false then the value in the attrs dict has
# priority instead
attrs = tag.get('attrs', '')
new_attrs = get_new_attrs(attrs)
# Insert the new attributes in their original position, in their original order in that attrs dict
for attr_name, attr_value in list(tag.attrib.items()):
# We have to rebuild the attributes to maintain their order
if attr_name == 'attrs':
# Insert the new attributes in their original position, in their original order
for new_attr, new_attr_value in new_attrs.items():
if new_attr in tag.attrib:
# Combine attribute if present as a separate attribute as well as in the 'attrs' dict
# This is the same behaviour that Odoo 16- uses in this situation
# Since we can't know what the reason for the double attribute definition is we
# combine both values regardless, even if the old value is simply 'True' or '1', so
# the condition in the attrs dict is not lost
old_attr_value = tag.attrib.get(new_attr)
if old_attr_value in [True, 1, 'True', '1']:
new_attr_value = f"True or ({new_attr_value})"
elif old_attr_value in [False, 0, 'False', '0']:
new_attr_value = f"False or ({new_attr_value})"
else:
new_attr_value = f"({old_attr_value}) or ({new_attr_value})"
all_attributes.append((new_attr, new_attr_value))
elif attr_name not in new_attrs:
# Add all other attributes in their same position. We skip the attributes also present in the
# 'attrs' dict since they will have been merged into those 'attrs' attributes
all_attributes.append((attr_name, attr_value))
tag.attrib.clear()
tag.attrib.update(all_attributes)
# Management of <attributes name="attrs">... overrides
attribute_tags_with_attrs_after = []
for attribute_tag in attribute_tags_with_attrs:
tag_type = get_inherited_tag_type(doc, attribute_tag)
tag_index, parent_tag, indent = get_parent_etree_node(doc, attribute_tag)
tail = attribute_tag.tail or ''
attrs = attribute_tag.text or ''
new_attrs = get_new_attrs(attrs)
attribute_tags_to_remove = []
# Insert the new attributes tags in their original position, in their original order in that attrs dict
for new_attr, new_attr_value in new_attrs.items():
if (separate_attr_tag := get_sibling_attribute_tag_of_type(doc, attribute_tag, new_attr)) is not None:
attribute_tags_to_remove.append(separate_attr_tag)
# Combine attribute if present as a separate attribute as well as in the 'attrs' dict
# This is the same behaviour that Odoo 16- uses in this situation
# Since we can't know what the reason for the double attribute definition is we
# combine both values regardless, even if the old value is simply 'True' or '1', so
# the condition in the attrs dict is not lost
old_attr_value = separate_attr_tag.text
if old_attr_value in [True, 1, 'True', '1']:
new_attr_value = f"True or ({new_attr_value})"
elif old_attr_value in [False, 0, 'False', '0']:
new_attr_value = f"False or ({new_attr_value})"
else:
new_attr_value = f"({old_attr_value}) or ({new_attr_value})"
new_tag = etree.Element('attribute', attrib={
'name': new_attr
})
new_tag.text = str(new_attr_value)
# First set the tail so that all following new attribute tags have the same indentation
new_tag.tail = indent
parent_tag.insert(tag_index, new_tag)
if new_attr == 'invisible':
if get_sibling_attribute_tag_of_type(doc, new_tag, 'states') is None:
# Since before Odoo 17 the states and invisible attributes were separate, if a states attribute was
# present in a parent view it would still be combined with the invisible attribute overrides
# in inheriting views. Now that they are combined in 17, if in an inheriting view the invisible
# attribute is overridden but not the states attribute, simply converting the invisible override
# to a separate attribute would actually override any states attributes in parent views as well.
# Since we can't automatically check the inheritance tree to account for this, a TODO is added
todo_tag = etree.Comment(
f"TODO: Result from 'attrs' -> 'invisible' conversion without also overriding 'states' attribute"
f"{indent + (' ' * 5)}Check if this {tag_type + ' ' if tag_type else ''}tag contained a states attribute in any of the parent views, in which case it should be combined into this 'invisible' attribute"
f"{indent + (' ' * 5)}(If any states attributes existed in parent views, they'll also be marked with a TODO)")
todo_tag.tail = indent
parent_tag.insert(tag_index, todo_tag)
attribute_tags_with_attrs_after.append(todo_tag)
tag_index += 1
attribute_tags_with_attrs_after.append(new_tag)
tag_index += 1
missing_attrs = []
if tag_type == 'field':
potentially_missing_attrs = NEW_ATTRS
else:
# Only field tags can use readonly, required and column_invisible attributes
potentially_missing_attrs = ['invisible']
for missing_attr in potentially_missing_attrs:
if missing_attr not in new_attrs and get_sibling_attribute_tag_of_type(doc, attribute_tag, missing_attr) is None:
# Only consider attribute missing if it's not in the 'attrs' dict and also not in a separate attribute tag
missing_attrs.append(missing_attr)
if missing_attrs:
# Before Odoo 17, overriding one attribute (for example 'readonly') in an 'attrs' would mean you had
# to include the other attributes present in the 'attrs'. Any attributes not present in the
# inheriting 'attr' attribute would be considered overridden with an empty value. To ensure the
# conversion keeps this behaviour, and because we can't know which attributes are and aren't present
# in the 'attrs' in the parent views, we have to add all missing attributes as empty tags to be
# safe.
# if attribute_tag_inherits_field(doc, attribute_tag):
if tag_type == 'field':
new_tag = etree.Comment(
f"TODO: Result from converting 'attrs' attribute override without options for {missing_attrs} to separate attributes"
f"{indent + (' ' * 5)}Remove redundant empty tags below for any of those attributes that are not present in the field tag in any of the parent views"
f"{indent + (' ' * 5)}If someone later adds one of these attributes in the parent views, they would likely be unaware it's still overridden in this view, resulting in unexpected behaviour, which should be avoided")
new_tag.tail = indent
parent_tag.insert(tag_index, new_tag)
attribute_tags_with_attrs_after.append(new_tag)
tag_index += 1
else:
# For non-field tags the 'attrs' attribute normally only contains an 'invisible' option, so
# if there's missing attributes (which would be just the 'invisible' attribute) it means it was
# done deliberately, so we don't need the TODO
pass
for missing_attr in missing_attrs:
new_tag = etree.Element('attribute', attrib={
'name': missing_attr
})
# First set the tail so that all following new attribute tags have the same indentation
new_tag.tail = indent
parent_tag.insert(tag_index, new_tag)
if missing_attr == 'invisible':
if get_sibling_attribute_tag_of_type(doc, new_tag, 'states') is None:
# Since before Odoo 17 the states and invisible attributes were separate, if a states attribute was
# present in an parent view it would still be combined with the invisible attribute overrides
# in inheriting views. Now that they are combined in 17, if in an inheriting view the invisible
# attribute is overridden but not the states attribute, simply converting the invisible override
# to a separate attribute would actually override any states attributes in parent views as well.
# Since we can't automatically check the inheritance tree to account for this, a TODO is added
todo_tag = etree.Comment(
f"TODO: Result from 'attrs' -> 'invisible' conversion without also overriding 'states' attribute"
f"{indent + (' ' * 5)}Check if this {tag_type + ' ' if tag_type else ''}tag contained a states attribute in any of the parent views, that should be combined into this 'invisible' attribute"
f"{indent + (' ' * 5)}(If any states attributes existed in parent views, they'll also be marked with a TODO)")
todo_tag.tail = indent
parent_tag.insert(tag_index, todo_tag)
attribute_tags_with_attrs_after.append(todo_tag)
tag_index += 1
attribute_tags_with_attrs_after.append(new_tag)
tag_index += 1
# Then set the tail of the last added tag so that the following tags maintain their original indentation
new_tag.tail = tail
parent_tag.remove(attribute_tag)
# For attributes that have been combined, the original separate attribute tag has to be removed
# We do so while maintaining the correct indentation
for attribute_tag_to_remove in attribute_tags_to_remove:
tag_index, parent_tag, indent = get_parent_etree_node(doc, attribute_tag_to_remove)
if tag_index > 0:
# Not necessary if tag has index 0 since this guarantees at least 1 tag with the same
# indentation follows
previous_tag = get_child_tag_at_index(parent_tag, tag_index - 1)
previous_tag.tail = attribute_tag_to_remove.tail
parent_tag.remove(attribute_tag_to_remove)
# Management of tags that have states=""
for state_tag in tags_with_states:
states_attribute = state_tag.get('states', '')
invisible_attribute = state_tag.get('invisible', '')
# Since before Odoo 17 the states and invisible attributes were separate, if an invisible attribute
# was overridden/added in inheriting views it would still be combined with the states attribute
# from parent views. Now that they are combined in 17, if in an inheriting view the invisible
# attribute was overridden/added but not the states attribute, the states condition would have to
# be added in as well. Since we can't automatically check the inheritance tree to account for this
# automatically, a TODO is added
tag_index, parent_tag, indent = get_parent_etree_node(doc, state_tag)
if invisible_attribute:
conversion_action_string = f"Result from merging \"states='{states_attribute}'\" attribute with an 'invisible' attribute"
else:
conversion_action_string = f"Result from converting \"states='{states_attribute}'\" attribute into an 'invisible' attribute"
todo_tag = etree.Comment(
f"TODO: {conversion_action_string}"
f"{indent + (' ' * 5)}Manually combine states condition into any 'invisible' overrides in inheriting views as well")
todo_tag.tail = indent
parent_tag.insert(tag_index, todo_tag)
new_invisible_attribute = get_combined_invisible_condition(invisible_attribute, states_attribute)
all_attributes = []
for attr_name, attr_value in list(state_tag.attrib.items()):
# We have to rebuild the attributes to maintain their order
if attr_name == 'invisible' or (attr_name == 'states' and not invisible_attribute):
# Update invisible attribute if it exists, else replace the states attribute
if new_invisible_attribute:
all_attributes.append(('invisible', new_invisible_attribute))
elif attr_name != 'states':
# Don't keep the states attribute
all_attributes.append((attr_name, attr_value))
state_tag.attrib.clear()
state_tag.attrib.update(all_attributes)
# Management of <attribute name="states">... overrides
attribute_tags_with_states_after = []
for attribute_tag_states in attribute_tags_with_states:
tag_type = get_inherited_tag_type(doc, attribute_tag_states)
tag_index, parent_tag, indent = get_parent_etree_node(doc, attribute_tag_states)
tail = attribute_tag_states.tail
attribute_tag_invisible = get_sibling_attribute_tag_of_type(doc, attribute_tag_states, 'invisible')
if attribute_tag_invisible is not None:
# If the states tag is merged into an invisible tag, the tail of the previous tag
# has to be updated, since otherwise the tag after the states tag will get indented the
# same as the states tag
if tag_index > 0:
# Not necessary if states tag has index 0 since this guarantees at least the invisible tag
# with the same indentation follows
previous_tag = get_child_tag_at_index(parent_tag, tag_index - 1)
previous_tag.tail = attribute_tag_states.tail
else:
# Since before Odoo 17 the states and invisible attributes were separate, if an invisible attribute
# was present in an parent view it would still be combined with the states attribute overrides
# in inheriting views. Now that they are combined in 17, if in an inheriting view the states
# attribute is overridden but not the invisible attribute, simply converting the states override
# to an invisible attribute would actually override any invisible attributes in parent views as well.
# Since we can't automatically check the inheritance tree to account for this, a TODO is added
todo_tag = etree.Comment(
f"TODO: Result from \"states='{states_attribute}'\" -> 'invisible' conversion without also overriding 'attrs' attribute"
f"{indent + (' ' * 5)}Check if this {tag_type + ' ' if tag_type else ''}tag contains an invisible attribute in any of the parent views, in which case it should be combined into this new 'invisible' attribute"
f"{indent + (' ' * 5)}(Only applies to invisible attributes in the parent views that were not originally states attributes. Those from converted states attributes will be marked with a TODO)")
todo_tag.tail = indent
parent_tag.insert(tag_index, todo_tag)
attribute_tags_with_states_after.append(todo_tag)
tag_index += 1
# If no invisible attribute tag exists, add it in place of the original states attribute tag
attribute_tag_invisible = etree.Element('attribute', attrib={'name': 'invisible'})
attribute_tag_invisible.tail = tail
parent_tag.insert(tag_index, attribute_tag_invisible)
invisible_attribute = attribute_tag_invisible.text or ''
states_attribute = attribute_tag_states.text or ''
invisible_condition = get_combined_invisible_condition(invisible_attribute, states_attribute)
parent_tag.remove(attribute_tag_states)
attribute_tag_invisible.text = invisible_condition
attribute_tags_with_states_after.append(attribute_tag_invisible)
print('\n##### Will be replaced by #####\n')
for t in tags_with_attrs + attribute_tags_with_attrs_after + tags_with_states + attribute_tags_with_states_after:
print(etree.tostring(t, encoding='unicode'))
print('\n###############################\n')
if autoreplace.lower()[0] == 'n':
confirm = input('Do you want to replace? (y/n) (empty == no) : ') or 'n'
else:
confirm = 'y'
if confirm.lower()[0] == 'y':
with open(xml_file, 'wb') as rf:
xml_string = etree.tostring(doc, encoding='utf-8', xml_declaration=has_encoding_declaration)
if convert_line_separator_back_to_windows:
xml_string = xml_string.replace(b"\n", b"\r\n")
rf.write(xml_string)
ok_files.append(xml_file)
except Exception as e:
nok_files.append((xml_file, e))
raise e
print('\n################################################')
print('################## Run Debug ##################')
print('################################################')
if nofilesfound:
print('No XML Files with "attrs" or "states" found in dir " %s "' % root_dir)
print('Succeeded on files')
for file in ok_files:
print(file)
if not ok_files:
print('No files')
print('')
print('Failed on files')
for file in nok_files:
print(file[0])
print('Reason: ', file[1])
if not nok_files:
print('No files')