Skip to content

Commit 4a9b85e

Browse files
jeanseb6windrjarry
authored andcommitted
xpath: add support for indexed xpath
This patch adds support for indexed xpath to xpath_set. In the case of a leaf-list, it is now possible to apply xpath_set in this way: xpath_set(d, "/lstnum[5]", 33, after="4") This will replace the 5th element of the leaf-list with the new value 33. This feature is important because libyang can return this type of xpath when a callback is called with sr_module_change_subscribe from sysrepo. The tests are updated accordingly. Signed-off-by: Jean-Sébastien Bevilacqua <jean-sebastien.bevilacqua@6wind.com>
1 parent c3305ab commit 4a9b85e

File tree

2 files changed

+75
-37
lines changed

2 files changed

+75
-37
lines changed

libyang/xpath.py

Lines changed: 69 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Copyright (c) 2020 6WIND S.A.
22
# SPDX-License-Identifier: MIT
33

4+
import contextlib
45
import fnmatch
56
import re
67
from typing import Any, Dict, Iterator, List, Optional, Tuple, Union
@@ -56,17 +57,28 @@ def xpath_split(xpath: str) -> Iterator[Tuple[str, str, List[Tuple[str, str]]]]:
5657
while i < len(xpath) and xpath[i] == "[":
5758
i += 1 # skip opening '['
5859
j = xpath.find("=", i) # find key name end
59-
key_name = xpath[i:j]
60-
quote = xpath[j + 1] # record opening quote character
61-
j = i = j + 2 # skip '=' and opening quote
62-
while True:
63-
if xpath[j] == quote and xpath[j - 1] != "\\":
64-
break
65-
j += 1
66-
# replace escaped chars by their non-escape version
67-
key_value = xpath[i:j].replace(f"\\{quote}", f"{quote}")
68-
keys.append((key_name, key_value))
69-
i = j + 2 # skip closing quote and ']'
60+
61+
if j != -1: # keyed specifier
62+
key_name = xpath[i:j]
63+
quote = xpath[j + 1] # record opening quote character
64+
j = i = j + 2 # skip '=' and opening quote
65+
while True:
66+
if xpath[j] == quote and xpath[j - 1] != "\\":
67+
break
68+
j += 1
69+
# replace escaped chars by their non-escape version
70+
key_value = xpath[i:j].replace(f"\\{quote}", f"{quote}")
71+
keys.append((key_name, key_value))
72+
i = j + 2 # skip closing quote and ']'
73+
else: # index specifier
74+
j = i
75+
while True:
76+
if xpath[j] == "]":
77+
break
78+
j += 1
79+
key_value = xpath[i:j]
80+
keys.append(("", key_value))
81+
i = j + 2
7082

7183
yield prefix, name, keys
7284

@@ -134,6 +146,12 @@ def _list_find_key_index(keys: List[Tuple[str, str]], lst: List) -> int:
134146
if py_to_yang(elem) == keys[0][1]:
135147
return i
136148

149+
elif keys[0][0] == "":
150+
# keys[0][1] is directly the index
151+
index = int(keys[0][1]) - 1
152+
if len(lst) > index:
153+
return index
154+
137155
else:
138156
for i, elem in enumerate(lst):
139157
if not isinstance(elem, dict):
@@ -410,32 +428,47 @@ def xpath_set(
410428
lst.append(value)
411429
return lst[key_val]
412430

413-
if isinstance(lst, list):
414-
# regular python list, need to iterate over it
415-
try:
416-
i = _list_find_key_index(keys, lst)
417-
# found
418-
if force:
419-
lst[i] = value
420-
return lst[i]
421-
except ValueError:
422-
# not found
423-
if after is None:
424-
lst.append(value)
425-
elif after == "":
426-
lst.insert(0, value)
427-
else:
428-
if after[0] != "[":
429-
after = "[.=%r]" % str(after)
430-
_, _, after_keys = next(xpath_split("/*" + after))
431-
insert_index = _list_find_key_index(after_keys, lst) + 1
432-
if insert_index == len(lst):
433-
lst.append(value)
434-
else:
435-
lst.insert(insert_index, value)
436-
return value
431+
# regular python list from now
432+
if not isinstance(lst, list):
433+
raise TypeError("expected a list")
434+
435+
with contextlib.suppress(ValueError):
436+
i = _list_find_key_index(keys, lst)
437+
# found
438+
if force:
439+
lst[i] = value
440+
return lst[i]
441+
442+
# value not found; handle insertion based on 'after'
443+
if after is None:
444+
lst.append(value)
445+
return value
446+
447+
if after == "":
448+
lst.insert(0, value)
449+
return value
450+
451+
# first try to find the value in the leaf list
452+
try:
453+
_, _, after_keys = next(
454+
xpath_split(f"/*{after}" if after[0] == "[" else f"/*[.={after!r}]")
455+
)
456+
insert_index = _list_find_key_index(after_keys, lst) + 1
457+
except ValueError:
458+
# handle 'after' as numeric index
459+
if not after.isnumeric():
460+
raise
461+
462+
insert_index = int(after)
463+
if insert_index > len(lst):
464+
raise
465+
466+
if insert_index == len(lst):
467+
lst.append(value)
468+
else:
469+
lst.insert(insert_index, value)
437470

438-
raise TypeError("expected a list")
471+
return value
439472

440473

441474
# -------------------------------------------------------------------------------------

tests/test_xpath.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ def test_xpath_set(self):
4141
)
4242
ly.xpath_set(d, "/lstnum[.='100']", 100)
4343
ly.xpath_set(d, "/lstnum[.='1']", 1, after="")
44+
ly.xpath_set(d, "/lstnum[5]", 33, after="4")
45+
ly.xpath_set(d, "/lstnum[5]", 34, after="4")
46+
ly.xpath_set(d, "/lstnum[5]", 35, after="4")
47+
ly.xpath_set(d, "/lstnum[7]", 101, after="6")
48+
ly.xpath_set(d, "/lstnum[8]", 102, after="7")
4449
with self.assertRaises(ValueError):
4550
ly.xpath_set(d, "/lstnum[.='1000']", 1000, after="1000000")
4651
with self.assertRaises(ValueError):
@@ -101,7 +106,7 @@ def test_xpath_set(self):
101106
{"name": "eth3", "mtu": 1000},
102107
],
103108
"lst2": ["a", "b", "c"],
104-
"lstnum": [1, 10, 20, 30, 40, 100],
109+
"lstnum": [1, 10, 20, 30, 35, 100, 101, 102],
105110
"val": 43,
106111
},
107112
)

0 commit comments

Comments
 (0)