forked from pierrelocus/odoo-attrs-replace
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathreplace_attrs.py
243 lines (227 loc) · 11.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
# -*- coding: utf-8 -*-
import re
from bs4 import formatter, BeautifulSoup as bs
from pathlib import Path
xml_4indent_formatter = formatter.XMLFormatter(indent=4)
NEW_ATTRS = {'required', 'invisible', 'readonly', 'column_invisible'}
percent_d_regex = re.compile("%\('?\"?[\w\.\d_]+'?\"?\)d")
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"""
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):
stringify = ''
switcher = False
# Replace operators not supported in python (=, like, ilike)
operator = str(leaf[1])
if operator == '=':
operator = '=='
elif 'like' in operator:
if 'not' in operator:
operator = 'not in'
else:
operator = 'in'
switcher = True
# 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]
if right_operand in ('True', 'False', '1', '0') or type(right_operand) in (list, tuple, set, int, float, bool):
right_operand = str(right_operand)
else:
right_operand = "'"+right_operand+"'"
stringify = "%s %s %s" % (right_operand if switcher else left_operand, operator, left_operand if switcher else right_operand)
return stringify
def stringify_attr(stack):
if stack in (True, False, 'True', 'False', 1, 0, '1', '0'):
return 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 == '&' or leaf_or_operator == '|':
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):
new_attrs = {}
attrs_dict = eval(attrs.strip())
for attr in NEW_ATTRS:
if attr in attrs_dict.keys():
new_attrs[attr] = stringify_attr(attrs_dict[attr])
return new_attrs
# Prettify puts <attribute> on three lines (1/ opening tag, 2/ text, 3/ closing tag), not very cool.
# Taken from https://stackoverflow.com/questions/55962146/remove-line-breaks-and-spaces-around-span-elements-with-python-regex
# And changed to avoid putting ALL one line, and only manage <attribute>, as it's the only one messing stuff here
# Kinda ugly to use the 3 types of tags but tbh I keep it like this while I have no time for a regex replace keeping the name="x" :p
def prettify_output(html):
for attr in NEW_ATTRS:
html = re.sub(f'<attribute name="{attr}">[ \n]+',f'<attribute name="{attr}">', html)
html = re.sub(f'[ \n]+</attribute>',f'</attribute>', html)
html = re.sub(r'<field name="([a-z_]+)">[ \n]+', r'<field name="\1">', html)
html = re.sub(r'[ \n]+</field>', r'</field>', html)
return html
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 = []
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
counter_for_percent_d_replace = 1
percent_d_results = {}
for percent_d in percent_d_regex.findall(contents):
contents = contents.replace(percent_d, "'REPLACEME%s'" % counter_for_percent_d_replace)
percent_d_results[counter_for_percent_d_replace] = percent_d
counter_for_percent_d_replace += 1
soup = bs(contents, 'xml')
tags_with_attrs = soup.select('[attrs]')
attribute_tags_name_attrs = soup.select('attribute[name="attrs"]')
tags_with_states = soup.select('[states]')
attribute_tags_name_states = soup.select('attribute[name="states"]')
if not (tags_with_attrs or attribute_tags_name_attrs or\
tags_with_states or attribute_tags_name_states):
continue
print('\n################################################################')
print('##### Taking care of file -> %s' % xml_file)
print('\n########### Current tags found ###\n')
for t in tags_with_attrs + attribute_tags_name_attrs + tags_with_states + attribute_tags_name_states:
print(t)
nofilesfound = False
# Management of tags that have attrs=""
for tag in tags_with_attrs:
attrs = tag['attrs']
new_attrs = get_new_attrs(attrs)
del tag['attrs']
for new_attr in new_attrs.keys():
tag[new_attr] = new_attrs[new_attr]
# Management of attributes name="attrs"
attribute_tags_after = []
for attribute_tag in attribute_tags_name_attrs:
new_attrs = get_new_attrs(attribute_tag.text)
for new_attr in new_attrs.keys():
new_tag = soup.new_tag('attribute')
new_tag['name'] = new_attr
new_tag.append(str(new_attrs[new_attr]))
attribute_tags_after.append(new_tag)
attribute_tag.insert_after(new_tag)
attribute_tag.decompose()
# Management ot tags that have states=""
for state_tag in tags_with_states:
base_invisible = ''
if 'invisible' in state_tag.attrs and state_tag['invisible']:
base_invisible = state_tag['invisible']
if not (base_invisible.endswith('or') or base_invisible.endswith('and')):
base_invisible = base_invisible + ' or '
else:
base_invisible = base_invisible + ' '
invisible_attr = "state not in [%s]" % ','.join(("'" + state.strip() + "'") for state in state_tag['states'].split(','))
state_tag['invisible'] = base_invisible + invisible_attr
del state_tag['states']
# Management of attributes name="states"
attribute_tags_states_after = []
for attribute_tag_states in attribute_tags_name_states:
states = attribute_tag_states.text
existing_invisible_tag = False
# I don't know why, looking for attribute[name="invisible"] does not work,
# but if it exists, I can find it with findAll attribute -> loop to name="invisible"
for tag in attribute_tag_states.parent.findAll('attribute'):
if tag['name'] == 'invisible':
existing_invisible_tag = tag
break
if not existing_invisible_tag:
existing_invisible_tag = soup.new_tag('attribute')
existing_invisible_tag['name'] = 'invisible'
if existing_invisible_tag.text:
states_to_add = 'state not in [%s]' % (
','.join(("'" + state.strip() + "'") for state in states.split(','))
)
if existing_invisible_tag.text.endswith('or') or existing_invisible_tag.text.endswith('and'):
new_invisible_text = '%s %s' % (existing_invisible_tag.text, states_to_add)
else:
new_invisible_text = ' or '.join([existing_invisible_tag.text, states_to_add])
else:
new_invisible_text = 'state not in [%s]' % (
','.join(("'" + state.strip() + "'") for state in states.split(','))
)
existing_invisible_tag.string = new_invisible_text
attribute_tag_states.insert_after(existing_invisible_tag)
attribute_tag_states.decompose()
attribute_tags_states_after.append(existing_invisible_tag)
print('\n########### Will be replaced by ###\n')
for t in tags_with_attrs + attribute_tags_after + tags_with_states + attribute_tags_states_after:
print(t)
print('################################################################\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:
html = soup.prettify(formatter=xml_4indent_formatter)
html = prettify_output(html)
for percent_d_result in percent_d_results.keys():
html = html.replace("'REPLACEME%s'" % percent_d_result, percent_d_results[percent_d_result])
rf.write(html.encode('utf-8'))
ok_files.append(xml_file)
except Exception as e:
nok_files.append((xml_file, 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')