3 # Copyright 2016 RIFT.IO Inc
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
22 class Attribute(collections
.namedtuple("Attribute", "module name")):
24 return "{}:{}".format(self
.module
, self
.name
)
27 class ListElement(collections
.namedtuple("List", "module name key value")):
29 return "{}:{}[{}={}]".format(self
.module
, self
.name
, self
.key
, self
.value
)
33 """Return a list of tokens representing an xpath
35 The types of xpaths that this selector supports is extremely limited.
36 The xpath is required to be an absolute path delimited by a
37 forward-slash. Each of the parts (elements between delimiters) is
38 treated as one of two possible types:
43 An attribute is a normal python attribute on an object. A list element
44 is an element within a list, which is identified by a key value (like a
45 yang list, although this is more properly a dict in python).
47 Each attribute is expected to have the form,
49 <namespace>:<variable-name>
51 A valid variable name (or namespace) follows the python regular expression,
55 A list entry has the form,
57 <namespace>:<variable-name>[<namespace>:<variable-name>=<value>]
59 The expression in the square brackets is the key of the required
60 element, and the value that that key must have.
63 xpath - a string containing an xpath expression
66 A ValueError is raised if the xpath cannot be parsed.
72 # define the symbols that are valid for a variable name in yang
73 name
= "[a-zA-Z0-9-_]+"
75 # define a set of regular expressions for parsing the xpath
76 pattern_attribute
= re
.compile("({t}):({t})$".format(t
=name
))
77 pattern_key_value
= re
.compile("^{t}:({t})\s*=\s*(.*)$".format(t
=name
))
78 pattern_quote
= re
.compile("^[\'\"](.*)[\'\"]$")
79 pattern_list
= re
.compile("^(.*)\[(.*)\]$")
81 def dash_to_underscore(text
):
82 return text
.replace('-', '_')
84 # Iterate through the parts of the xpath (NB: because the xpaths are
85 # required to be absolute paths, the first character is going to be the
86 # forward slash. As a result, when the string is split, the first
87 # element with be an empty string).
89 for part
in xpath
.split("/")[1:]:
91 # Test the part to see if it is a attribute
92 result
= pattern_attribute
.match(part
)
93 if result
is not None:
94 module
, name
= result
.groups()
96 # Convert the dashes to underscores
97 name
= dash_to_underscore(name
)
98 module
= dash_to_underscore(module
)
100 tokens
.append(Attribute(module
, name
))
104 # Test the part to see if it is a list
105 result
= pattern_list
.match(part
)
106 if result
is not None:
107 attribute
, keyvalue
= result
.groups()
109 module
, name
= pattern_attribute
.match(attribute
).groups()
110 key
, value
= pattern_key_value
.match(keyvalue
).groups()
112 # Convert the dashes to underscore (but not in the key value)
113 key
= dash_to_underscore(key
)
114 name
= dash_to_underscore(name
)
115 module
= dash_to_underscore(module
)
117 result
= pattern_quote
.match(value
)
118 if result
is not None:
119 value
= result
.group(1)
121 tokens
.append(ListElement(module
, name
, key
, value
))
125 raise ValueError("cannot parse '{}'".format(part
))
130 class XPathAttribute(object):
132 This class is used to represent a reference to an attribute. If you use
133 getattr on an attribute, it may give you the value of the attribute rather
134 than a reference to it. What is really wanted is a representation of the
135 attribute so that its value can be both retrieved and set. That is what
139 def __init__(self
, obj
, name
):
140 """Create an instance of XPathAttribute
143 obj - the object containing the attribute
144 name - the name of an attribute
147 A ValueError is raised if the provided object does not have the
148 associated attribute.
151 if not hasattr(obj
, name
):
152 msg
= "The provided object does not contain the associated attribute"
153 raise ValueError(msg
)
163 return getattr(self
.obj
, self
.name
)
166 def value(self
, value
):
167 """Set the value of the attribute
170 value - the new value that the attribute should take
173 An TypeError is raised if the provided value cannot be cast the
174 current type of the attribute.
177 attr_type
= type(self
.value
)
180 # The only way we can currently get the type of the atrribute is if it
181 # has an existing value. So if the attribute has an existing value,
182 # cast the value to the type of the attribute value.
183 if attr_type
is not type(None):
185 attr_value
= attr_type(attr_value
)
188 msg
= "expected type '{}', but got '{}' instead"
189 raise TypeError(msg
.format(attr_type
.__name
__, type(value
).__name
__))
191 setattr(self
.obj
, self
.name
, attr_value
)
194 class XPathElement(XPathAttribute
):
196 This class is used to represent a reference to an element within a list.
197 Unlike scalar attributes, it is not entirely necessary to have this class
198 to represent the attribute because the element cannot be a simple scalar.
199 However, this class is used because it creates a uniform interface that can
200 be used by the setxattr and getxattr functions.
203 def __init__(self
, container
, key
, value
):
204 """Create an instance of XPathElement
207 container - the object that contains the element
208 key - the name of the field that is used to identify the
210 value - the value of the key that identifies the element
213 self
._container
= container
219 for element
in self
._container
:
220 if getattr(element
, self
._key
) == self
._value
:
223 raise ValueError("specified element does not exist")
226 def value(self
, value
):
228 for element
in self
._container
:
229 if getattr(element
, self
._key
) == self
._value
:
233 if existing
is not None:
234 self
._container
.remove(existing
)
236 self
._container
.append(value
)
239 class XPathSelector(object):
240 def __init__(self
, xpath
):
241 """Creates an instance of XPathSelector
244 xpath - a string containing an xpath expression
247 self
._tokens
= tokenize(xpath
)
250 def __call__(self
, obj
):
251 """Returns a reference to an attribute on the provided object
253 Using the defined xpath, an attribute is selected from the provided
260 A ValueError is raised if the specified element in a list cannot be
264 an XPathAttribute that reference the specified attribute
268 for token
in self
._tokens
[:-1]:
269 # If the object is contained within a list, we will need to iterate
270 # through the tokens until we find a token that is a field of the
272 if token
.name
not in current
.fields
:
276 raise ValueError('cannot find attribute {}'.format(token
.name
))
278 # If the token is a ListElement, try to find the matching element
279 if isinstance(token
, ListElement
):
280 for element
in getattr(current
, token
.name
):
281 if getattr(element
, token
.key
) == token
.value
:
286 raise ValueError('unable to find {}'.format(token
.value
))
289 # Attribute the variable matching the name of the token
290 current
= getattr(current
, token
.name
)
292 # Process the final token
293 token
= self
._tokens
[-1]
295 # If the token represents a list element, find the element in the list
296 # and return an XPathElement
297 if isinstance(token
, ListElement
):
298 container
= getattr(current
, token
.name
)
299 for element
in container
:
300 if getattr(element
, token
.key
) == token
.value
:
301 return XPathElement(container
, token
.key
, token
.value
)
304 raise ValueError('unable to find {}'.format(token
.value
))
306 # Otherwise, return the object as an XPathAttribute
307 return XPathAttribute(current
, token
.name
)
311 """The tokens in the xpath expression"""
315 # A global cache to avoid repeated parsing of known xpath expressions
316 __xpath_cache
= dict()
321 __xpath_cache
= dict()
324 def getxattr(obj
, xpath
):
325 """Return an attribute on the provided object
327 The xpath is parsed and used to identify an attribute on the provided
328 object. The object is expected to be a GI object where each attribute that
329 is accessible via an xpath expression is contained in the 'fields'
330 attribute of the object (NB: this is not true of GI lists, which do not
331 have a 'fields' attribute).
333 A selector is create for each xpath and used to find the specified
334 attribute. The accepted xpath expressions are those supported by the
335 XPathSelector class. The parsed xpath expression is cached so that
336 subsequent parsing is unnecessary. However, selectors are stored in a
337 global dictionary and this means that this function is not thread-safe.
341 xpath - a string containing an xpath expression
344 an attribute on the provided object
347 if xpath
not in __xpath_cache
:
348 __xpath_cache
[xpath
] = XPathSelector(xpath
)
350 return __xpath_cache
[xpath
](obj
).value
353 def setxattr(obj
, xpath
, value
):
354 """Set the attribute referred to by the xpath
358 xpath - a string containing an xpath expression
359 value - the new value of the attribute
362 if xpath
not in __xpath_cache
:
363 __xpath_cache
[xpath
] = XPathSelector(xpath
)
365 __xpath_cache
[xpath
](obj
).value
= value