# -*- coding: utf-8 -*- import re from bs4 import BeautifulSoup as bs from ast import literal_eval import logging # Set up logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Constants NEW_ATTRS = {'required', 'invisible', 'readonly', 'column_invisible'} def upgrade(file_manager): """Upgrade XML files by converting 'attrs' and 'states' into new attributes.""" # Filter files to only XML files files = [file for file in file_manager if file.path.suffix == '.xml'] if not files: return # Regex patterns with re.VERBOSE for readability percent_d_re = re.compile(r""" % # Start with percent \( # Opening parenthesis '? # Optional single quote "? # Optional double quote (?P[\w\.\d_]+) # Capture key (word chars, dots, digits, underscores) '? # Optional single quote "? # Optional double quote \) # Closing parenthesis d # Ends with 'd' """, re.VERBOSE) # Helper functions def normalize_domain(domain): if len(domain) == 1: return domain result = [] expected = 1 op_arity = {'!': 1, '&': 2, '|': 2} for token in domain: if expected == 0: result[0:0] = ['&'] expected = 1 if isinstance(token, (list, tuple)): expected -= 1 token = tuple(token) else: expected += op_arity.get(token, 0) - 1 result.append(token) return result def stringify_leaf(leaf): operator = str(leaf[1]) if operator == '=': operator = '==' elif 'like' in operator: operator = 'not in' if 'not' in operator else 'in' return f"{leaf[2] if isinstance(leaf[2], str) else str(leaf[2])} {operator} {leaf[0]}" right = str(leaf[2]) if isinstance(leaf[2], (list, tuple, set, int, float, bool)) or leaf[2] in ('True', 'False', '1', '0') else f"'{leaf[2]}'" return f"{leaf[0]} {operator} {right}" def stringify_attr(stack): if stack in (True, False, 'True', 'False', 1, 0, '1', '0'): return str(stack) last_paren_idx = max(i for i, item in enumerate(stack[::-1]) if item not in ('|', '!')) stack = normalize_domain(stack)[::-1] result = [] for idx, item in enumerate(stack): if item == '!': expr = result.pop() result.append(f"(not ({expr}))") elif item in ('&', '|'): try: left, right = result.pop(), result.pop() op = 'and' if item == '&' else 'or' form = f"({left} {op} {right})" if idx > last_paren_idx else f"{left} {op} {right}" result.append(form) except IndexError: result.append(f"{left} {op}") else: result.append(stringify_leaf(item)) return result[0] def get_new_attrs(attrs): try: # Use ast.literal_eval for safe evaluation of Python literals attrs_dict = literal_eval(attrs.strip()) if not isinstance(attrs_dict, dict): logger.warning(f"Invalid attrs format, expected dict, got {attrs_dict}") return {} return {attr: stringify_attr(attrs_dict[attr]) for attr in NEW_ATTRS if attr in attrs_dict} except (ValueError, SyntaxError) as e: logger.error(f"Failed to parse attrs '{attrs}': {e}") return {} except Exception as e: logger.error(f"Unexpected error parsing attrs '{attrs}': {e}") return {} # Process each file for fileno, file in enumerate(files, start=1): content = file.content # Handle percent-d replacements percent_d_results = {} for i, match in enumerate(percent_d_re.findall(content), 1): placeholder = f"'REPLACEME{i}'" percent_d_results[i] = match[0] # Full match content = content.replace(match[0], placeholder) # Parse XML with BeautifulSoup soup = bs(content, 'xml') tags_with_attrs = soup.select('[attrs]') attr_tags = soup.select('attribute[name="attrs"]') tags_with_states = soup.select('[states]') state_tags = soup.select('attribute[name="states"]') if not (tags_with_attrs or attr_tags or tags_with_states or state_tags): continue # Process tags with attrs for tag in tags_with_attrs: new_attrs = get_new_attrs(tag['attrs']) if new_attrs: # Only proceed if parsing succeeded del tag['attrs'] for attr, value in new_attrs.items(): tag[attr] = value else: logger.warning(f"Skipping tag in {file.path} due to invalid attrs: {tag.get('attrs')}") # Process for attr_tag in attr_tags: new_attrs = get_new_attrs(attr_tag.text) if new_attrs: for attr, value in new_attrs.items(): new_tag = soup.new_tag('attribute', name=attr) new_tag.string = value attr_tag.insert_after(new_tag) attr_tag.decompose() else: logger.warning(f"Skipping attribute tag in {file.path} due to invalid attrs: {attr_tag.text}") # Process tags with states for tag in tags_with_states: base_invisible = tag.get('invisible', '') if base_invisible and not base_invisible.endswith(('or', 'and')): base_invisible += ' or ' states = [f"'{s.strip()}'" for s in tag['states'].split(',')] tag['invisible'] = f"{base_invisible}state not in [{','.join(states)}]" del tag['states'] # Process for state_tag in state_tags: states = [f"'{s.strip()}'" for s in state_tag.text.split(',')] parent = state_tag.parent inv_tag = next((t for t in parent.findAll('attribute') if t['name'] == 'invisible'), None) if not inv_tag: inv_tag = soup.new_tag('attribute', name='invisible') current = inv_tag.text or '' new_text = f"state not in [{','.join(states)}]" inv_tag.string = f"{current} or {new_text}" if current else new_text state_tag.insert_after(inv_tag) state_tag.decompose() # Write back with percent-d restored output = str(soup) for i, original in percent_d_results.items(): output = output.replace(f"'REPLACEME{i}'", original) file.content = output file_manager.print_progress(fileno, len(files))