172 lines
6.8 KiB
Python
172 lines
6.8 KiB
Python
# -*- 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<key>[\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 <attribute name="attrs">
|
|
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 <attribute name="states">
|
|
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)) |