blob: da0b3f3ef36034f9029e44590806c8528015887d [file] [log] [blame]
# Copyright 2023 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Functions to parse and modify control files when updating METADATA."""
import ast
import json
def format_string_value(s, indent=8, target_length=60):
"""Format a string value for printing in a control file.
Args:
s: the string to be formatted
indent: the indent when multiple lines are needed
target_length: the target line length for the file
Returns:
A formatted string.
"""
value = ' '.join(s.split()) # Remove excess spaces/newlines.
if len(value) < target_length:
return f'"{value}"'
printed_lines = []
value = value.replace('"', '\"')
words = value.split(' ')
curr = ''
for word in words:
if len(curr) + len(word) + 1 < target_length:
curr += word + ' '
else:
printed_lines.append(curr.strip())
curr = word + ' '
if curr != '':
printed_lines.append(curr.strip())
join_value = '"\n' + ' ' * indent + '"'
return f'("{join_value.join(printed_lines)}")'
def format_list_value(l, indent=8):
"""Format a list value for printing in a control file.
Args:
l: the list to be formatted
indent: the indent when multiple lines are needed
Returns:
A formatted string representing the list.
"""
if len(l) <= 1:
return json.dumps(l)
# For lists with multiple string values, split them onto multiple lines.
# E.g.: [\n"foo",\n "bar",\n ]
value = json.dumps(l, indent=indent)
return value.replace('\n]', ',\n ]')
def format_metadata(metadata_dict):
"""Return a formatted string representing the given metadata.
Args:
metadata_dict: the metadata to be formatted
Returns:
A formatted string of the form 'METADATA = {...}'. This output can be
swapped in with the previous METADATA declaraction in a file.
"""
inner_values = ''
for key in metadata_dict:
value = metadata_dict[key]
printed_value = json.dumps(value)
if isinstance(value, list):
printed_value = format_list_value(value)
if isinstance(value, str):
printed_value = format_string_value(value, indent=len(key)+9)
if isinstance(value, bool):
printed_value = "True" if value else "False"
inner_values += f' "{key}": {printed_value},\n'
return f'METADATA = {{\n{inner_values}}}'
class ControlFile():
"""Class representing a Control file to be edited (or skipped)."""
def __init__(self, path):
self.path = path
self.name_value = ''
self.contents = '' # The contents of the file
self.metadata_start = -1 # The index of the M in METADATA
self.metadata_end = -1 # The index after the closing }
self.isChanged = False
self.metadata = {}
self.is_valid = self.find_metadata_elt()
def find_metadata_elt(self):
"""Parse the file and locate METADATA = ..., if present.
Returns:
True if the metadata declaration was found, else False.
"""
with open(self.path, encoding='utf-8') as f:
self.contents = f.read()
if not self.contents:
return False
parsed_file = ast.parse(self.contents)
metadata_elt = None
for elt in parsed_file.body:
if (isinstance(elt, ast.Assign) and
len(elt.targets) > 0 and
isinstance(elt.targets[0], ast.Name)):
first_target = ast.Name(elt.targets[0].id)
if (first_target.id == 'METADATA' and
isinstance(elt.value, ast.Dict)):
metadata_elt = elt
if (first_target.id == 'NAME' and
isinstance(elt.value, ast.Constant) and
isinstance(elt.value.value, str)):
self.name_value = elt.value.value
if not metadata_elt:
return False
# Caclulate file offsets for the METADATA declaration.
# Note that ast only reports offsets into a specific line, while
# we need the offset into the entire file.
lines = self.contents.split("\n")
file_offset = 0
for i, _ in enumerate(lines):
if i == metadata_elt.lineno - 1:
self.metadata_start = file_offset + metadata_elt.col_offset
if i == metadata_elt.end_lineno - 1:
self.metadata_end = file_offset + metadata_elt.end_col_offset
break
file_offset += len(lines[i])+1
self.metadata = ast.literal_eval(metadata_elt.value)
return True
def update_contents(self):
"""Modify self.contents using the modified values in self.metadata."""
new_metadata = format_metadata(self.metadata)
self.contents = (self.contents[:self.metadata_start] +
new_metadata +
self.contents[self.metadata_end:])