forked from robotframework/robotframework
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathXML.py
More file actions
1511 lines (1214 loc) · 69.1 KB
/
Copy pathXML.py
File metadata and controls
1511 lines (1214 loc) · 69.1 KB
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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Copyright 2008-2015 Nokia Networks
# Copyright 2016- Robot Framework Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import copy
import re
import os
try:
from lxml import etree as lxml_etree
except ImportError:
lxml_etree = None
from robot.api import logger
from robot.libraries.BuiltIn import BuiltIn
from robot.utils import (asserts, ET, ETSource, is_falsy, is_string, is_truthy,
plural_or_not as s)
from robot.version import get_version
should_be_equal = asserts.assert_equal
should_match = BuiltIn().should_match
class XML(object):
"""Robot Framework test library for verifying and modifying XML documents.
As the name implies, _XML_ is a test library for verifying contents of XML
files. In practice it is a pretty thin wrapper on top of Python's
[http://docs.python.org/library/xml.etree.elementtree.html|ElementTree XML API].
The library has the following main usages:
- Parsing an XML file, or a string containing XML, into an XML element
structure and finding certain elements from it for for further analysis
(e.g. `Parse XML` and `Get Element` keywords).
- Getting text or attributes of elements
(e.g. `Get Element Text` and `Get Element Attribute`).
- Directly verifying text, attributes, or whole elements
(e.g `Element Text Should Be` and `Elements Should Be Equal`).
- Modifying XML and saving it (e.g. `Set Element Text`, `Add Element`
and `Save XML`).
== Table of contents ==
- `Parsing XML`
- `Using lxml`
- `Example`
- `Finding elements with xpath`
- `Element attributes`
- `Handling XML namespaces`
- `Boolean arguments`
- `Pattern matching`
- `Shortcuts`
- `Keywords`
= Parsing XML =
XML can be parsed into an element structure using `Parse XML` keyword.
It accepts both paths to XML files and strings that contain XML. The
keyword returns the root element of the structure, which then contains
other elements as its children and their children. Possible comments and
processing instructions in the source XML are removed.
XML is not validated during parsing even if has a schema defined. How
possible doctype elements are handled otherwise depends on the used XML
module and on the platform. The standard ElementTree strips doctypes
altogether but when `using lxml` they are preserved when XML is saved.
The element structure returned by `Parse XML`, as well as elements
returned by keywords such as `Get Element`, can be used as the ``source``
argument with other keywords. In addition to an already parsed XML
structure, other keywords also accept paths to XML files and strings
containing XML similarly as `Parse XML`. Notice that keywords that modify
XML do not write those changes back to disk even if the source would be
given as a path to a file. Changes must always saved explicitly using
`Save XML` keyword.
When the source is given as a path to a file, the forward slash character
(``/``) can be used as the path separator regardless the operating system.
On Windows also the backslash works, but it the test data it needs to be
escaped by doubling it (``\\\\``). Using the built-in variable ``${/}``
naturally works too.
= Using lxml =
By default this library uses Python's standard
[http://docs.python.org/library/xml.etree.elementtree.html|ElementTree]
module for parsing XML, but it can be configured to use
[http://lxml.de|lxml] module instead when `importing` the library.
The resulting element structure has same API regardless which module
is used for parsing.
The main benefits of using lxml is that it supports richer xpath syntax
than the standard ElementTree and enables using `Evaluate Xpath` keyword.
It also preserves the doctype and possible namespace prefixes saving XML.
= Example =
The following simple example demonstrates parsing XML and verifying its
contents both using keywords in this library and in _BuiltIn_ and
_Collections_ libraries. How to use xpath expressions to find elements
and what attributes the returned elements contain are discussed, with
more examples, in `Finding elements with xpath` and `Element attributes`
sections.
In this example, as well as in many other examples in this documentation,
``${XML}`` refers to the following example XML document. In practice
``${XML}`` could either be a path to an XML file or it could contain the XML
itself.
| <example>
| <first id="1">text</first>
| <second id="2">
| <child/>
| </second>
| <third>
| <child>more text</child>
| <second id="child"/>
| <child><grandchild/></child>
| </third>
| <html>
| <p>
| Text with <b>bold</b> and <i>italics</i>.
| </p>
| </html>
| </example>
| ${root} = | `Parse XML` | ${XML} | | |
| `Should Be Equal` | ${root.tag} | example | | |
| ${first} = | `Get Element` | ${root} | first | |
| `Should Be Equal` | ${first.text} | text | | |
| `Dictionary Should Contain Key` | ${first.attrib} | id | |
| `Element Text Should Be` | ${first} | text | | |
| `Element Attribute Should Be` | ${first} | id | 1 | |
| `Element Attribute Should Be` | ${root} | id | 1 | xpath=first |
| `Element Attribute Should Be` | ${XML} | id | 1 | xpath=first |
Notice that in the example three last lines are equivalent. Which one to
use in practice depends on which other elements you need to get or verify.
If you only need to do one verification, using the last line alone would
suffice. If more verifications are needed, parsing the XML with `Parse XML`
only once would be more efficient.
= Finding elements with xpath =
ElementTree, and thus also this library, supports finding elements using
xpath expressions. ElementTree does not, however, support the full xpath
standard. The supported xpath syntax is explained below and
[https://docs.python.org/library/xml.etree.elementtree.html#xpath-support|
ElementTree documentation] provides more details. In the examples
``${XML}`` refers to the same XML structure as in the earlier example.
If lxml support is enabled when `importing` the library, the whole
[http://www.w3.org/TR/xpath/|xpath 1.0 standard] is supported.
That includes everything listed below but also lot of other useful
constructs.
== Tag names ==
When just a single tag name is used, xpath matches all direct child
elements that have that tag name.
| ${elem} = | `Get Element` | ${XML} | third |
| `Should Be Equal` | ${elem.tag} | third | |
| @{children} = | `Get Elements` | ${elem} | child |
| `Length Should Be` | ${children} | 2 | |
== Paths ==
Paths are created by combining tag names with a forward slash (``/``). For
example, ``parent/child`` matches all ``child`` elements under ``parent``
element. Notice that if there are multiple ``parent`` elements that all
have ``child`` elements, ``parent/child`` xpath will match all these
``child`` elements.
| ${elem} = | `Get Element` | ${XML} | second/child |
| `Should Be Equal` | ${elem.tag} | child | |
| ${elem} = | `Get Element` | ${XML} | third/child/grandchild |
| `Should Be Equal` | ${elem.tag} | grandchild | |
== Wildcards ==
An asterisk (``*``) can be used in paths instead of a tag name to denote
any element.
| @{children} = | `Get Elements` | ${XML} | */child |
| `Length Should Be` | ${children} | 3 | |
== Current element ==
The current element is denoted with a dot (``.``). Normally the current
element is implicit and does not need to be included in the xpath.
== Parent element ==
The parent element of another element is denoted with two dots (``..``).
Notice that it is not possible to refer to the parent of the current
element.
| ${elem} = | `Get Element` | ${XML} | */second/.. |
| `Should Be Equal` | ${elem.tag} | third | |
== Search all sub elements ==
Two forward slashes (``//``) mean that all sub elements, not only the
direct children, are searched. If the search is started from the current
element, an explicit dot is required.
| @{elements} = | `Get Elements` | ${XML} | .//second |
| `Length Should Be` | ${elements} | 2 | |
| ${b} = | `Get Element` | ${XML} | html//b |
| `Should Be Equal` | ${b.text} | bold | |
== Predicates ==
Predicates allow selecting elements using also other criteria than tag
names, for example, attributes or position. They are specified after the
normal tag name or path using syntax ``path[predicate]``. The path can have
wildcards and other special syntax explained earlier. What predicates
the standard ElementTree supports is explained in the table below.
| = Predicate = | = Matches = | = Example = |
| @attrib | Elements with attribute ``attrib``. | second[@id] |
| @attrib="value" | Elements with attribute ``attrib`` having value ``value``. | *[@id="2"] |
| position | Elements at the specified position. Position can be an integer (starting from 1), expression ``last()``, or relative expression like ``last() - 1``. | third/child[1] |
| tag | Elements with a child element named ``tag``. | third/child[grandchild] |
Predicates can also be stacked like ``path[predicate1][predicate2]``.
A limitation is that possible position predicate must always be first.
= Element attributes =
All keywords returning elements, such as `Parse XML`, and `Get Element`,
return ElementTree's
[http://docs.python.org/library/xml.etree.elementtree.html#element-objects|Element objects].
These elements can be used as inputs for other keywords, but they also
contain several useful attributes that can be accessed directly using
the extended variable syntax.
The attributes that are both useful and convenient to use in the test
data are explained below. Also other attributes, including methods, can
be accessed, but that is typically better to do in custom libraries than
directly in the test data.
The examples use the same ``${XML}`` structure as the earlier examples.
== tag ==
The tag of the element.
| ${root} = | `Parse XML` | ${XML} |
| `Should Be Equal` | ${root.tag} | example |
== text ==
The text that the element contains or Python ``None`` if the element has no
text. Notice that the text _does not_ contain texts of possible child
elements nor text after or between children. Notice also that in XML
whitespace is significant, so the text contains also possible indentation
and newlines. To get also text of the possible children, optionally
whitespace normalized, use `Get Element Text` keyword.
| ${1st} = | `Get Element` | ${XML} | first |
| `Should Be Equal` | ${1st.text} | text | |
| ${2nd} = | `Get Element` | ${XML} | second/child |
| `Should Be Equal` | ${2nd.text} | ${NONE} | |
| ${p} = | `Get Element` | ${XML} | html/p |
| `Should Be Equal` | ${p.text} | \\n${SPACE*6}Text with${SPACE} |
== tail ==
The text after the element before the next opening or closing tag. Python
``None`` if the element has no tail. Similarly as with ``text``, also
``tail`` contains possible indentation and newlines.
| ${b} = | `Get Element` | ${XML} | html/p/b |
| `Should Be Equal` | ${b.tail} | ${SPACE}and${SPACE} |
== attrib ==
A Python dictionary containing attributes of the element.
| ${2nd} = | `Get Element` | ${XML} | second |
| `Should Be Equal` | ${2nd.attrib['id']} | 2 | |
| ${3rd} = | `Get Element` | ${XML} | third |
| `Should Be Empty` | ${3rd.attrib} | | |
= Handling XML namespaces =
ElementTree and lxml handle possible namespaces in XML documents by adding
the namespace URI to tag names in so called Clark Notation. That is
inconvenient especially with xpaths, and by default this library strips
those namespaces away and moves them to ``xmlns`` attribute instead. That
can be avoided by passing ``keep_clark_notation`` argument to `Parse XML`
keyword. Alternatively `Parse XML` supports stripping namespace information
altogether by using ``strip_namespaces`` argument. The pros and cons of
different approaches are discussed in more detail below.
== How ElementTree handles namespaces ==
If an XML document has namespaces, ElementTree adds namespace information
to tag names in [http://www.jclark.com/xml/xmlns.htm|Clark Notation]
(e.g. ``{http://ns.uri}tag``) and removes original ``xmlns`` attributes.
This is done both with default namespaces and with namespaces with a prefix.
How it works in practice is illustrated by the following example, where
``${NS}`` variable contains this XML document:
| <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
| xmlns="http://www.w3.org/1999/xhtml">
| <xsl:template match="/">
| <html></html>
| </xsl:template>
| </xsl:stylesheet>
| ${root} = | `Parse XML` | ${NS} | keep_clark_notation=yes |
| `Should Be Equal` | ${root.tag} | {http://www.w3.org/1999/XSL/Transform}stylesheet |
| `Element Should Exist` | ${root} | {http://www.w3.org/1999/XSL/Transform}template/{http://www.w3.org/1999/xhtml}html |
| `Should Be Empty` | ${root.attrib} |
As you can see, including the namespace URI in tag names makes xpaths
really long and complex.
If you save the XML, ElementTree moves namespace information back to
``xmlns`` attributes. Unfortunately it does not restore the original
prefixes:
| <ns0:stylesheet xmlns:ns0="http://www.w3.org/1999/XSL/Transform">
| <ns0:template match="/">
| <ns1:html xmlns:ns1="http://www.w3.org/1999/xhtml"></ns1:html>
| </ns0:template>
| </ns0:stylesheet>
The resulting output is semantically same as the original, but mangling
prefixes like this may still not be desirable. Notice also that the actual
output depends slightly on ElementTree version.
== Default namespace handling ==
Because the way ElementTree handles namespaces makes xpaths so complicated,
this library, by default, strips namespaces from tag names and moves that
information back to ``xmlns`` attributes. How this works in practice is
shown by the example below, where ``${NS}`` variable contains the same XML
document as in the previous example.
| ${root} = | `Parse XML` | ${NS} |
| `Should Be Equal` | ${root.tag} | stylesheet |
| `Element Should Exist` | ${root} | template/html |
| `Element Attribute Should Be` | ${root} | xmlns | http://www.w3.org/1999/XSL/Transform |
| `Element Attribute Should Be` | ${root} | xmlns | http://www.w3.org/1999/xhtml | xpath=template/html |
Now that tags do not contain namespace information, xpaths are simple again.
A minor limitation of this approach is that namespace prefixes are lost.
As a result the saved output is not exactly same as the original one in
this case either:
| <stylesheet xmlns="http://www.w3.org/1999/XSL/Transform">
| <template match="/">
| <html xmlns="http://www.w3.org/1999/xhtml"></html>
| </template>
| </stylesheet>
Also this output is semantically same as the original. If the original XML
had only default namespaces, the output would also look identical.
== Namespaces when using lxml ==
This library handles namespaces same way both when `using lxml` and when
not using it. There are, however, differences how lxml internally handles
namespaces compared to the standard ElementTree. The main difference is
that lxml stores information about namespace prefixes and they are thus
preserved if XML is saved. Another visible difference is that lxml includes
namespace information in child elements got with `Get Element` if the
parent element has namespaces.
== Stripping namespaces altogether ==
Because namespaces often add unnecessary complexity, `Parse XML` supports
stripping them altogether by using ``strip_namespaces=True``. When this
option is enabled, namespaces are not shown anywhere nor are they included
if XML is saved.
== Attribute namespaces ==
Attributes in XML documents are, by default, in the same namespaces as
the element they belong to. It is possible to use different namespaces
by using prefixes, but this is pretty rare.
If an attribute has a namespace prefix, ElementTree will replace it with
Clark Notation the same way it handles elements. Because stripping
namespaces from attributes could cause attribute conflicts, this library
does not handle attribute namespaces at all. Thus the following example
works the same way regardless how namespaces are handled.
| ${root} = | `Parse XML` | <root id="1" ns:id="2" xmlns:ns="http://my.ns"/> |
| `Element Attribute Should Be` | ${root} | id | 1 |
| `Element Attribute Should Be` | ${root} | {http://my.ns}id | 2 |
= Boolean arguments =
Some keywords accept arguments that are handled as Boolean values true or
false. If such an argument is given as a string, it is considered false if
it is an empty string or equal to ``FALSE``, ``NONE``, ``NO``, ``OFF`` or
``0``, case-insensitively. Other strings are considered true regardless
their value, and other argument types are tested using the same
[http://docs.python.org/library/stdtypes.html#truth|rules as in Python].
True examples:
| `Parse XML` | ${XML} | keep_clark_notation=True | # Strings are generally true. |
| `Parse XML` | ${XML} | keep_clark_notation=yes | # Same as the above. |
| `Parse XML` | ${XML} | keep_clark_notation=${TRUE} | # Python ``True`` is true. |
| `Parse XML` | ${XML} | keep_clark_notation=${42} | # Numbers other than 0 are true. |
False examples:
| `Parse XML` | ${XML} | keep_clark_notation=False | # String ``false`` is false. |
| `Parse XML` | ${XML} | keep_clark_notation=no | # Also string ``no`` is false. |
| `Parse XML` | ${XML} | keep_clark_notation=${EMPTY} | # Empty string is false. |
| `Parse XML` | ${XML} | keep_clark_notation=${FALSE} | # Python ``False`` is false. |
Considering string ``NONE`` false is new in Robot Framework 3.0.3 and
considering also ``OFF`` and ``0`` false is new in Robot Framework 3.1.
== Pattern matching ==
Some keywords, for example `Elements Should Match`, support so called
[http://en.wikipedia.org/wiki/Glob_(programming)|glob patterns] where:
| ``*`` | matches any string, even an empty string |
| ``?`` | matches any single character |
| ``[chars]`` | matches one character in the bracket |
| ``[!chars]`` | matches one character not in the bracket |
| ``[a-z]`` | matches one character from the range in the bracket |
| ``[!a-z]`` | matches one character not from the range in the bracket |
Unlike with glob patterns normally, path separator characters ``/`` and
``\\`` and the newline character ``\\n`` are matches by the above
wildcards.
Support for brackets like ``[abc]`` and ``[!a-z]`` is new in
Robot Framework 3.1
"""
ROBOT_LIBRARY_SCOPE = 'GLOBAL'
ROBOT_LIBRARY_VERSION = get_version()
_xml_declaration = re.compile('^<\?xml .*\?>')
def __init__(self, use_lxml=False):
"""Import library with optionally lxml mode enabled.
By default this library uses Python's standard
[http://docs.python.org/library/xml.etree.elementtree.html|ElementTree]
module for parsing XML. If ``use_lxml`` argument is given a true value
(see `Boolean arguments`), the library will use [http://lxml.de|lxml]
module instead. See `Using lxml` section for benefits provided by lxml.
Using lxml requires that the lxml module is installed on the system.
If lxml mode is enabled but the module is not installed, this library
will emit a warning and revert back to using the standard ElementTree.
"""
use_lxml = is_truthy(use_lxml)
if use_lxml and lxml_etree:
self.etree = lxml_etree
self.modern_etree = True
self.lxml_etree = True
else:
self.etree = ET
self.modern_etree = ET.VERSION >= '1.3'
self.lxml_etree = False
if use_lxml and not lxml_etree:
logger.warn('XML library reverted to use standard ElementTree '
'because lxml module is not installed.')
self._ns_stripper = NameSpaceStripper(self.etree, self.lxml_etree)
def parse_xml(self, source, keep_clark_notation=False, strip_namespaces=False):
"""Parses the given XML file or string into an element structure.
The ``source`` can either be a path to an XML file or a string
containing XML. In both cases the XML is parsed into ElementTree
[http://docs.python.org/library/xml.etree.elementtree.html#element-objects|element structure]
and the root element is returned. Possible comments and processing
instructions in the source XML are removed.
As discussed in `Handling XML namespaces` section, this keyword, by
default, removes namespace information ElementTree has added to tag
names and moves it into ``xmlns`` attributes. This typically eases
handling XML documents with namespaces considerably. If you do not
want that to happen, or want to avoid the small overhead of going
through the element structure when your XML does not have namespaces,
you can disable this feature by giving ``keep_clark_notation`` argument
a true value (see `Boolean arguments`).
If you want to strip namespace information altogether so that it is
not included even if XML is saved, you can give a true value to
``strip_namespaces`` argument. This functionality is new in Robot
Framework 3.0.2.
Examples:
| ${root} = | Parse XML | <root><child/></root> |
| ${xml} = | Parse XML | ${CURDIR}/test.xml | keep_clark_notation=True |
| ${xml} = | Parse XML | ${CURDIR}/test.xml | strip_namespaces=True |
Use `Get Element` keyword if you want to get a certain element and not
the whole structure. See `Parsing XML` section for more details and
examples.
"""
with ETSource(source) as source:
tree = self.etree.parse(source)
if self.lxml_etree:
strip = (lxml_etree.Comment, lxml_etree.ProcessingInstruction)
lxml_etree.strip_elements(tree, *strip, **dict(with_tail=False))
root = tree.getroot()
if not is_truthy(keep_clark_notation):
self._ns_stripper.strip(root, preserve=is_falsy(strip_namespaces))
return root
def get_element(self, source, xpath='.'):
"""Returns an element in the ``source`` matching the ``xpath``.
The ``source`` can be a path to an XML file, a string containing XML, or
an already parsed XML element. The ``xpath`` specifies which element to
find. See the `introduction` for more details about both the possible
sources and the supported xpath syntax.
The keyword fails if more, or less, than one element matches the
``xpath``. Use `Get Elements` if you want all matching elements to be
returned.
Examples using ``${XML}`` structure from `Example`:
| ${element} = | Get Element | ${XML} | second |
| ${child} = | Get Element | ${element} | child |
`Parse XML` is recommended for parsing XML when the whole structure
is needed. It must be used if there is a need to configure how XML
namespaces are handled.
Many other keywords use this keyword internally, and keywords modifying
XML are typically documented to both to modify the given source and
to return it. Modifying the source does not apply if the source is
given as a string. The XML structure parsed based on the string and
then modified is nevertheless returned.
"""
elements = self.get_elements(source, xpath)
if len(elements) != 1:
self._raise_wrong_number_of_matches(len(elements), xpath)
return elements[0]
def _raise_wrong_number_of_matches(self, count, xpath, message=None):
if not message:
message = self._wrong_number_of_matches(count, xpath)
raise AssertionError(message)
def _wrong_number_of_matches(self, count, xpath):
if not count:
return "No element matching '%s' found." % xpath
if count == 1:
return "One element matching '%s' found." % xpath
return "Multiple elements (%d) matching '%s' found." % (count, xpath)
def get_elements(self, source, xpath):
"""Returns a list of elements in the ``source`` matching the ``xpath``.
The ``source`` can be a path to an XML file, a string containing XML, or
an already parsed XML element. The ``xpath`` specifies which element to
find. See the `introduction` for more details.
Elements matching the ``xpath`` are returned as a list. If no elements
match, an empty list is returned. Use `Get Element` if you want to get
exactly one match.
Examples using ``${XML}`` structure from `Example`:
| ${children} = | Get Elements | ${XML} | third/child |
| Length Should Be | ${children} | 2 | |
| ${children} = | Get Elements | ${XML} | first/child |
| Should Be Empty | ${children} | | |
"""
if is_string(source):
source = self.parse_xml(source)
finder = ElementFinder(self.etree, self.modern_etree, self.lxml_etree)
return finder.find_all(source, xpath)
def get_child_elements(self, source, xpath='.'):
"""Returns the child elements of the specified element as a list.
The element whose children to return is specified using ``source`` and
``xpath``. They have exactly the same semantics as with `Get Element`
keyword.
All the direct child elements of the specified element are returned.
If the element has no children, an empty list is returned.
Examples using ``${XML}`` structure from `Example`:
| ${children} = | Get Child Elements | ${XML} | |
| Length Should Be | ${children} | 4 | |
| ${children} = | Get Child Elements | ${XML} | xpath=first |
| Should Be Empty | ${children} | | |
"""
return list(self.get_element(source, xpath))
def get_element_count(self, source, xpath='.'):
"""Returns and logs how many elements the given ``xpath`` matches.
Arguments ``source`` and ``xpath`` have exactly the same semantics as
with `Get Elements` keyword that this keyword uses internally.
See also `Element Should Exist` and `Element Should Not Exist`.
"""
count = len(self.get_elements(source, xpath))
logger.info("%d element%s matched '%s'." % (count, s(count), xpath))
return count
def element_should_exist(self, source, xpath='.', message=None):
"""Verifies that one or more element match the given ``xpath``.
Arguments ``source`` and ``xpath`` have exactly the same semantics as
with `Get Elements` keyword. Keyword passes if the ``xpath`` matches
one or more elements in the ``source``. The default error message can
be overridden with the ``message`` argument.
See also `Element Should Not Exist` as well as `Get Element Count`
that this keyword uses internally.
"""
count = self.get_element_count(source, xpath)
if not count:
self._raise_wrong_number_of_matches(count, xpath, message)
def element_should_not_exist(self, source, xpath='.', message=None):
"""Verifies that no element match the given ``xpath``.
Arguments ``source`` and ``xpath`` have exactly the same semantics as
with `Get Elements` keyword. Keyword fails if the ``xpath`` matches any
element in the ``source``. The default error message can be overridden
with the ``message`` argument.
See also `Element Should Exist` as well as `Get Element Count`
that this keyword uses internally.
"""
count = self.get_element_count(source, xpath)
if count:
self._raise_wrong_number_of_matches(count, xpath, message)
def get_element_text(self, source, xpath='.', normalize_whitespace=False):
"""Returns all text of the element, possibly whitespace normalized.
The element whose text to return is specified using ``source`` and
``xpath``. They have exactly the same semantics as with `Get Element`
keyword.
This keyword returns all the text of the specified element, including
all the text its children and grandchildren contain. If the element
has no text, an empty string is returned. The returned text is thus not
always the same as the `text` attribute of the element.
By default all whitespace, including newlines and indentation, inside
the element is returned as-is. If ``normalize_whitespace`` is given
a true value (see `Boolean arguments`), then leading and trailing
whitespace is stripped, newlines and tabs converted to spaces, and
multiple spaces collapsed into one. This is especially useful when
dealing with HTML data.
Examples using ``${XML}`` structure from `Example`:
| ${text} = | Get Element Text | ${XML} | first |
| Should Be Equal | ${text} | text | |
| ${text} = | Get Element Text | ${XML} | second/child |
| Should Be Empty | ${text} | | |
| ${paragraph} = | Get Element | ${XML} | html/p |
| ${text} = | Get Element Text | ${paragraph} | normalize_whitespace=yes |
| Should Be Equal | ${text} | Text with bold and italics. |
See also `Get Elements Texts`, `Element Text Should Be` and
`Element Text Should Match`.
"""
element = self.get_element(source, xpath)
text = ''.join(self._yield_texts(element))
if is_truthy(normalize_whitespace):
text = self._normalize_whitespace(text)
return text
def _yield_texts(self, element, top=True):
if element.text:
yield element.text
for child in element:
for text in self._yield_texts(child, top=False):
yield text
if element.tail and not top:
yield element.tail
def _normalize_whitespace(self, text):
return ' '.join(text.split())
def get_elements_texts(self, source, xpath, normalize_whitespace=False):
"""Returns text of all elements matching ``xpath`` as a list.
The elements whose text to return is specified using ``source`` and
``xpath``. They have exactly the same semantics as with `Get Elements`
keyword.
The text of the matched elements is returned using the same logic
as with `Get Element Text`. This includes optional whitespace
normalization using the ``normalize_whitespace`` option.
Examples using ``${XML}`` structure from `Example`:
| @{texts} = | Get Elements Texts | ${XML} | third/child |
| Length Should Be | ${texts} | 2 | |
| Should Be Equal | @{texts}[0] | more text | |
| Should Be Equal | @{texts}[1] | ${EMPTY} | |
"""
return [self.get_element_text(elem, normalize_whitespace=normalize_whitespace)
for elem in self.get_elements(source, xpath)]
def element_text_should_be(self, source, expected, xpath='.',
normalize_whitespace=False, message=None):
"""Verifies that the text of the specified element is ``expected``.
The element whose text is verified is specified using ``source`` and
``xpath``. They have exactly the same semantics as with `Get Element`
keyword.
The text to verify is got from the specified element using the same
logic as with `Get Element Text`. This includes optional whitespace
normalization using the ``normalize_whitespace`` option.
The keyword passes if the text of the element is equal to the
``expected`` value, and otherwise it fails. The default error message
can be overridden with the ``message`` argument. Use `Element Text
Should Match` to verify the text against a pattern instead of an exact
value.
Examples using ``${XML}`` structure from `Example`:
| Element Text Should Be | ${XML} | text | xpath=first |
| Element Text Should Be | ${XML} | ${EMPTY} | xpath=second/child |
| ${paragraph} = | Get Element | ${XML} | xpath=html/p |
| Element Text Should Be | ${paragraph} | Text with bold and italics. | normalize_whitespace=yes |
"""
text = self.get_element_text(source, xpath, normalize_whitespace)
should_be_equal(text, expected, message, values=False)
def element_text_should_match(self, source, pattern, xpath='.',
normalize_whitespace=False, message=None):
"""Verifies that the text of the specified element matches ``expected``.
This keyword works exactly like `Element Text Should Be` except that
the expected value can be given as a pattern that the text of the
element must match.
Pattern matching is similar as matching files in a shell with
``*``, ``?`` and ``[chars]`` acting as wildcards. See the
`Pattern matching` section for more information.
Examples using ``${XML}`` structure from `Example`:
| Element Text Should Match | ${XML} | t??? | xpath=first |
| ${paragraph} = | Get Element | ${XML} | xpath=html/p |
| Element Text Should Match | ${paragraph} | Text with * and *. | normalize_whitespace=yes |
"""
text = self.get_element_text(source, xpath, normalize_whitespace)
should_match(text, pattern, message, values=False)
def get_element_attribute(self, source, name, xpath='.', default=None):
"""Returns the named attribute of the specified element.
The element whose attribute to return is specified using ``source`` and
``xpath``. They have exactly the same semantics as with `Get Element`
keyword.
The value of the attribute ``name`` of the specified element is returned.
If the element does not have such element, the ``default`` value is
returned instead.
Examples using ``${XML}`` structure from `Example`:
| ${attribute} = | Get Element Attribute | ${XML} | id | xpath=first |
| Should Be Equal | ${attribute} | 1 | | |
| ${attribute} = | Get Element Attribute | ${XML} | xx | xpath=first | default=value |
| Should Be Equal | ${attribute} | value | | |
See also `Get Element Attributes`, `Element Attribute Should Be`,
`Element Attribute Should Match` and `Element Should Not Have Attribute`.
"""
return self.get_element(source, xpath).get(name, default)
def get_element_attributes(self, source, xpath='.'):
"""Returns all attributes of the specified element.
The element whose attributes to return is specified using ``source`` and
``xpath``. They have exactly the same semantics as with `Get Element`
keyword.
Attributes are returned as a Python dictionary. It is a copy of the
original attributes so modifying it has no effect on the XML structure.
Examples using ``${XML}`` structure from `Example`:
| ${attributes} = | Get Element Attributes | ${XML} | first |
| Dictionary Should Contain Key | ${attributes} | id | |
| ${attributes} = | Get Element Attributes | ${XML} | third |
| Should Be Empty | ${attributes} | | |
Use `Get Element Attribute` to get the value of a single attribute.
"""
return dict(self.get_element(source, xpath).attrib)
def element_attribute_should_be(self, source, name, expected, xpath='.',
message=None):
"""Verifies that the specified attribute is ``expected``.
The element whose attribute is verified is specified using ``source``
and ``xpath``. They have exactly the same semantics as with
`Get Element` keyword.
The keyword passes if the attribute ``name`` of the element is equal to
the ``expected`` value, and otherwise it fails. The default error
message can be overridden with the ``message`` argument.
To test that the element does not have a certain attribute, Python
``None`` (i.e. variable ``${NONE}``) can be used as the expected value.
A cleaner alternative is using `Element Should Not Have Attribute`.
Examples using ``${XML}`` structure from `Example`:
| Element Attribute Should Be | ${XML} | id | 1 | xpath=first |
| Element Attribute Should Be | ${XML} | id | ${NONE} | |
See also `Element Attribute Should Match` and `Get Element Attribute`.
"""
attr = self.get_element_attribute(source, name, xpath)
should_be_equal(attr, expected, message, values=False)
def element_attribute_should_match(self, source, name, pattern, xpath='.',
message=None):
"""Verifies that the specified attribute matches ``expected``.
This keyword works exactly like `Element Attribute Should Be` except
that the expected value can be given as a pattern that the attribute of
the element must match.
Pattern matching is similar as matching files in a shell with
``*``, ``?`` and ``[chars]`` acting as wildcards. See the
`Pattern matching` section for more information.
Examples using ``${XML}`` structure from `Example`:
| Element Attribute Should Match | ${XML} | id | ? | xpath=first |
| Element Attribute Should Match | ${XML} | id | c*d | xpath=third/second |
"""
attr = self.get_element_attribute(source, name, xpath)
if attr is None:
raise AssertionError("Attribute '%s' does not exist." % name)
should_match(attr, pattern, message, values=False)
def element_should_not_have_attribute(self, source, name, xpath='.', message=None):
"""Verifies that the specified element does not have attribute ``name``.
The element whose attribute is verified is specified using ``source``
and ``xpath``. They have exactly the same semantics as with
`Get Element` keyword.
The keyword fails if the specified element has attribute ``name``. The
default error message can be overridden with the ``message`` argument.
Examples using ``${XML}`` structure from `Example`:
| Element Should Not Have Attribute | ${XML} | id |
| Element Should Not Have Attribute | ${XML} | xxx | xpath=first |
See also `Get Element Attribute`, `Get Element Attributes`,
`Element Text Should Be` and `Element Text Should Match`.
"""
attr = self.get_element_attribute(source, name, xpath)
if attr is not None:
raise AssertionError(message or "Attribute '%s' exists and "
"has value '%s'." % (name, attr))
def elements_should_be_equal(self, source, expected, exclude_children=False,
normalize_whitespace=False):
"""Verifies that the given ``source`` element is equal to ``expected``.
Both ``source`` and ``expected`` can be given as a path to an XML file,
as a string containing XML, or as an already parsed XML element
structure. See `introduction` for more information about parsing XML in
general.
The keyword passes if the ``source`` element and ``expected`` element
are equal. This includes testing the tag names, texts, and attributes
of the elements. By default also child elements are verified the same
way, but this can be disabled by setting ``exclude_children`` to a
true value (see `Boolean arguments`).
All texts inside the given elements are verified, but possible text
outside them is not. By default texts must match exactly, but setting
``normalize_whitespace`` to a true value makes text verification
independent on newlines, tabs, and the amount of spaces. For more
details about handling text see `Get Element Text` keyword and
discussion about elements' `text` and `tail` attributes in the
`introduction`.
Examples using ``${XML}`` structure from `Example`:
| ${first} = | Get Element | ${XML} | first |
| Elements Should Be Equal | ${first} | <first id="1">text</first> |
| ${p} = | Get Element | ${XML} | html/p |
| Elements Should Be Equal | ${p} | <p>Text with <b>bold</b> and <i>italics</i>.</p> | normalize_whitespace=yes |
| Elements Should Be Equal | ${p} | <p>Text with</p> | exclude | normalize |
The last example may look a bit strange because the ``<p>`` element
only has text ``Text with``. The reason is that rest of the text
inside ``<p>`` actually belongs to the child elements. This includes
the ``.`` at the end that is the `tail` text of the ``<i>`` element.
See also `Elements Should Match`.
"""
self._compare_elements(source, expected, should_be_equal,
exclude_children, normalize_whitespace)
def elements_should_match(self, source, expected, exclude_children=False,
normalize_whitespace=False):
"""Verifies that the given ``source`` element matches ``expected``.
This keyword works exactly like `Elements Should Be Equal` except that
texts and attribute values in the expected value can be given as
patterns.
Pattern matching is similar as matching files in a shell with
``*``, ``?`` and ``[chars]`` acting as wildcards. See the
`Pattern matching` section for more information.
Examples using ``${XML}`` structure from `Example`:
| ${first} = | Get Element | ${XML} | first |
| Elements Should Match | ${first} | <first id="?">*</first> |
See `Elements Should Be Equal` for more examples.
"""
self._compare_elements(source, expected, should_match,
exclude_children, normalize_whitespace)
def _compare_elements(self, source, expected, comparator, exclude_children,
normalize_whitespace):
normalizer = self._normalize_whitespace \
if is_truthy(normalize_whitespace) else None
comparator = ElementComparator(comparator, normalizer, exclude_children)
comparator.compare(self.get_element(source), self.get_element(expected))
def set_element_tag(self, source, tag, xpath='.'):
"""Sets the tag of the specified element.
The element whose tag to set is specified using ``source`` and
``xpath``. They have exactly the same semantics as with `Get Element`
keyword. The resulting XML structure is returned, and if the ``source``
is an already parsed XML structure, it is also modified in place.
Examples using ``${XML}`` structure from `Example`:
| Set Element Tag | ${XML} | newTag |
| Should Be Equal | ${XML.tag} | newTag |
| Set Element Tag | ${XML} | xxx | xpath=second/child |
| Element Should Exist | ${XML} | second/xxx |
| Element Should Not Exist | ${XML} | second/child |
Can only set the tag of a single element. Use `Set Elements Tag` to set
the tag of multiple elements in one call.
"""
source = self.get_element(source)
self.get_element(source, xpath).tag = tag
return source
def set_elements_tag(self, source, tag, xpath='.'):
"""Sets the tag of the specified elements.
Like `Set Element Tag` but sets the tag of all elements matching
the given ``xpath``.
"""
for elem in self.get_elements(source, xpath):
self.set_element_tag(elem, tag)
def set_element_text(self, source, text=None, tail=None, xpath='.'):
"""Sets text and/or tail text of the specified element.
The element whose text to set is specified using ``source`` and
``xpath``. They have exactly the same semantics as with `Get Element`
keyword. The resulting XML structure is returned, and if the ``source``
is an already parsed XML structure, it is also modified in place.
Element's text and tail text are changed only if new ``text`` and/or
``tail`` values are given. See `Element attributes` section for more
information about `text` and `tail` in general.
Examples using ``${XML}`` structure from `Example`:
| Set Element Text | ${XML} | new text | xpath=first |
| Element Text Should Be | ${XML} | new text | xpath=first |
| Set Element Text | ${XML} | tail=& | xpath=html/p/b |
| Element Text Should Be | ${XML} | Text with bold&italics. | xpath=html/p | normalize_whitespace=yes |
| Set Element Text | ${XML} | slanted | !! | xpath=html/p/i |
| Element Text Should Be | ${XML} | Text with bold&slanted!! | xpath=html/p | normalize_whitespace=yes |
Can only set the text/tail of a single element. Use `Set Elements Text`
to set the text/tail of multiple elements in one call.
"""
source = self.get_element(source)