# -*- 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]) ordered_attrs = {attr: new_attrs[attr] for attr in NEW_ATTRS if attr in new_attrs} return ordered_attrs # Prettify puts 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 , 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'[ \n]+',f'', html) html = re.sub(f'[ \n]+',f'', html) html = re.sub(r'[ \n]+', r'', html) html = re.sub(r'[ \n]+', r'', 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')