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
|
#!/usr/bin/env python3
#
# override-default: preprocessor for XML interface definitions to interpret
# redundant entries (relative to path) with tag 'defaultValue' as an override
# directive. Must be called before build-command-templates, as the schema
# disallows redundancy.
#
# Copyright (C) 2021 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
#
# Use lxml xpath capability to find multiple elements with tag defaultValue
# relative to path; replace and remove to override the value.
import sys
import glob
import logging
from copy import deepcopy
from lxml import etree
debug = False
logger = logging.getLogger(__name__)
logs_handler = logging.StreamHandler()
logger.addHandler(logs_handler)
if debug:
logger.setLevel(logging.DEBUG)
else:
logger.setLevel(logging.INFO)
def clear_empty_path(el):
# on the odd chance of interleaved comments
tmp = [l for l in el if isinstance(l.tag, str)]
if not tmp:
p = el.getparent()
p.remove(el)
clear_empty_path(p)
def override_element(l: list):
"""
Allow multiple override elements; use the final one (in document order).
"""
if len(l) < 2:
logger.debug("passing list of single element to override_element")
return
# assemble list of leafNodes of overriding defaultValues, for later removal
parents = []
for el in l[1:]:
parents.append(el.getparent())
# replace element with final override
l[0].getparent().replace(l[0], l[-1])
# remove all but overridden element
for el in parents:
tmp = el.getparent()
tmp.remove(el)
clear_empty_path(tmp)
def merge_remaining(l: list, elementtree):
"""
Merge (now) single leaf node containing 'defaultValue' with leaf nodes
of same path and no 'defaultValue'.
"""
for p in l:
p = p.split()
path_str = f'/interfaceDefinition/*'
path_list = []
for i in range(len(p)):
path_list.append(f'[@name="{p[i]}"]')
path_str += '/children/*'.join(path_list)
rp = elementtree.xpath(path_str)
if len(rp) > 1:
for el in rp[1:]:
# in practice there will only be one child of the path,
# either defaultValue or Properties, since
# override_element() has already run
for child in el:
rp[0].append(deepcopy(child))
tmp = el.getparent()
tmp.remove(el)
clear_empty_path(tmp)
def collect_and_override(dir_name):
"""
Collect elements with defaultValue tag into dictionary indexed by name
attributes of ancestor path.
"""
for fname in glob.glob(f'{dir_name}/*.xml'):
tree = etree.parse(fname)
root = tree.getroot()
defv = {}
xpath_str = '//defaultValue'
xp = tree.xpath(xpath_str)
for element in xp:
ap = element.xpath('ancestor::*[@name]')
ap_name = [el.get("name") for el in ap]
ap_path_str = ' '.join(ap_name)
defv.setdefault(ap_path_str, []).append(element)
for k, v in defv.items():
if len(v) > 1:
logger.info(f"overriding default in path '{k}'")
override_element(v)
to_merge = list(defv)
merge_remaining(to_merge, tree)
revised_str = etree.tostring(root, encoding='unicode', pretty_print=True)
with open(f'{fname}', 'w') as f:
f.write(revised_str)
def main():
if len(sys.argv) < 2:
logger.critical('Must specify XML directory!')
sys.exit(1)
dir_name = sys.argv[1]
collect_and_override(dir_name)
if __name__ == '__main__':
main()
|