• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# regression test for SAX 2.0
2# $Id$
3
4from xml.sax import make_parser, ContentHandler, \
5                    SAXException, SAXReaderNotAvailable, SAXParseException
6import unittest
7from unittest import mock
8try:
9    make_parser()
10except SAXReaderNotAvailable:
11    # don't try to test this module if we cannot create a parser
12    raise unittest.SkipTest("no XML parsers available")
13from xml.sax.saxutils import XMLGenerator, escape, unescape, quoteattr, \
14                             XMLFilterBase, prepare_input_source
15from xml.sax.expatreader import create_parser
16from xml.sax.handler import feature_namespaces, feature_external_ges
17from xml.sax.xmlreader import InputSource, AttributesImpl, AttributesNSImpl
18from io import BytesIO, StringIO
19import codecs
20import os.path
21import shutil
22from urllib.error import URLError
23import urllib.request
24from test import support
25from test.support import findfile, run_unittest, FakePath, TESTFN
26
27TEST_XMLFILE = findfile("test.xml", subdir="xmltestdata")
28TEST_XMLFILE_OUT = findfile("test.xml.out", subdir="xmltestdata")
29try:
30    TEST_XMLFILE.encode("utf-8")
31    TEST_XMLFILE_OUT.encode("utf-8")
32except UnicodeEncodeError:
33    raise unittest.SkipTest("filename is not encodable to utf8")
34
35supports_nonascii_filenames = True
36if not os.path.supports_unicode_filenames:
37    try:
38        support.TESTFN_UNICODE.encode(support.TESTFN_ENCODING)
39    except (UnicodeError, TypeError):
40        # Either the file system encoding is None, or the file name
41        # cannot be encoded in the file system encoding.
42        supports_nonascii_filenames = False
43requires_nonascii_filenames = unittest.skipUnless(
44        supports_nonascii_filenames,
45        'Requires non-ascii filenames support')
46
47ns_uri = "http://www.python.org/xml-ns/saxtest/"
48
49class XmlTestBase(unittest.TestCase):
50    def verify_empty_attrs(self, attrs):
51        self.assertRaises(KeyError, attrs.getValue, "attr")
52        self.assertRaises(KeyError, attrs.getValueByQName, "attr")
53        self.assertRaises(KeyError, attrs.getNameByQName, "attr")
54        self.assertRaises(KeyError, attrs.getQNameByName, "attr")
55        self.assertRaises(KeyError, attrs.__getitem__, "attr")
56        self.assertEqual(attrs.getLength(), 0)
57        self.assertEqual(attrs.getNames(), [])
58        self.assertEqual(attrs.getQNames(), [])
59        self.assertEqual(len(attrs), 0)
60        self.assertNotIn("attr", attrs)
61        self.assertEqual(list(attrs.keys()), [])
62        self.assertEqual(attrs.get("attrs"), None)
63        self.assertEqual(attrs.get("attrs", 25), 25)
64        self.assertEqual(list(attrs.items()), [])
65        self.assertEqual(list(attrs.values()), [])
66
67    def verify_empty_nsattrs(self, attrs):
68        self.assertRaises(KeyError, attrs.getValue, (ns_uri, "attr"))
69        self.assertRaises(KeyError, attrs.getValueByQName, "ns:attr")
70        self.assertRaises(KeyError, attrs.getNameByQName, "ns:attr")
71        self.assertRaises(KeyError, attrs.getQNameByName, (ns_uri, "attr"))
72        self.assertRaises(KeyError, attrs.__getitem__, (ns_uri, "attr"))
73        self.assertEqual(attrs.getLength(), 0)
74        self.assertEqual(attrs.getNames(), [])
75        self.assertEqual(attrs.getQNames(), [])
76        self.assertEqual(len(attrs), 0)
77        self.assertNotIn((ns_uri, "attr"), attrs)
78        self.assertEqual(list(attrs.keys()), [])
79        self.assertEqual(attrs.get((ns_uri, "attr")), None)
80        self.assertEqual(attrs.get((ns_uri, "attr"), 25), 25)
81        self.assertEqual(list(attrs.items()), [])
82        self.assertEqual(list(attrs.values()), [])
83
84    def verify_attrs_wattr(self, attrs):
85        self.assertEqual(attrs.getLength(), 1)
86        self.assertEqual(attrs.getNames(), ["attr"])
87        self.assertEqual(attrs.getQNames(), ["attr"])
88        self.assertEqual(len(attrs), 1)
89        self.assertIn("attr", attrs)
90        self.assertEqual(list(attrs.keys()), ["attr"])
91        self.assertEqual(attrs.get("attr"), "val")
92        self.assertEqual(attrs.get("attr", 25), "val")
93        self.assertEqual(list(attrs.items()), [("attr", "val")])
94        self.assertEqual(list(attrs.values()), ["val"])
95        self.assertEqual(attrs.getValue("attr"), "val")
96        self.assertEqual(attrs.getValueByQName("attr"), "val")
97        self.assertEqual(attrs.getNameByQName("attr"), "attr")
98        self.assertEqual(attrs["attr"], "val")
99        self.assertEqual(attrs.getQNameByName("attr"), "attr")
100
101
102def xml_str(doc, encoding=None):
103    if encoding is None:
104        return doc
105    return '<?xml version="1.0" encoding="%s"?>\n%s' % (encoding, doc)
106
107def xml_bytes(doc, encoding, decl_encoding=...):
108    if decl_encoding is ...:
109        decl_encoding = encoding
110    return xml_str(doc, decl_encoding).encode(encoding, 'xmlcharrefreplace')
111
112def make_xml_file(doc, encoding, decl_encoding=...):
113    if decl_encoding is ...:
114        decl_encoding = encoding
115    with open(TESTFN, 'w', encoding=encoding, errors='xmlcharrefreplace') as f:
116        f.write(xml_str(doc, decl_encoding))
117
118
119class ParseTest(unittest.TestCase):
120    data = '<money value="$\xa3\u20ac\U0001017b">$\xa3\u20ac\U0001017b</money>'
121
122    def tearDown(self):
123        support.unlink(TESTFN)
124
125    def check_parse(self, f):
126        from xml.sax import parse
127        result = StringIO()
128        parse(f, XMLGenerator(result, 'utf-8'))
129        self.assertEqual(result.getvalue(), xml_str(self.data, 'utf-8'))
130
131    def test_parse_text(self):
132        encodings = ('us-ascii', 'iso-8859-1', 'utf-8',
133                     'utf-16', 'utf-16le', 'utf-16be')
134        for encoding in encodings:
135            self.check_parse(StringIO(xml_str(self.data, encoding)))
136            make_xml_file(self.data, encoding)
137            with open(TESTFN, 'r', encoding=encoding) as f:
138                self.check_parse(f)
139            self.check_parse(StringIO(self.data))
140            make_xml_file(self.data, encoding, None)
141            with open(TESTFN, 'r', encoding=encoding) as f:
142                self.check_parse(f)
143
144    def test_parse_bytes(self):
145        # UTF-8 is default encoding, US-ASCII is compatible with UTF-8,
146        # UTF-16 is autodetected
147        encodings = ('us-ascii', 'utf-8', 'utf-16', 'utf-16le', 'utf-16be')
148        for encoding in encodings:
149            self.check_parse(BytesIO(xml_bytes(self.data, encoding)))
150            make_xml_file(self.data, encoding)
151            self.check_parse(TESTFN)
152            with open(TESTFN, 'rb') as f:
153                self.check_parse(f)
154            self.check_parse(BytesIO(xml_bytes(self.data, encoding, None)))
155            make_xml_file(self.data, encoding, None)
156            self.check_parse(TESTFN)
157            with open(TESTFN, 'rb') as f:
158                self.check_parse(f)
159        # accept UTF-8 with BOM
160        self.check_parse(BytesIO(xml_bytes(self.data, 'utf-8-sig', 'utf-8')))
161        make_xml_file(self.data, 'utf-8-sig', 'utf-8')
162        self.check_parse(TESTFN)
163        with open(TESTFN, 'rb') as f:
164            self.check_parse(f)
165        self.check_parse(BytesIO(xml_bytes(self.data, 'utf-8-sig', None)))
166        make_xml_file(self.data, 'utf-8-sig', None)
167        self.check_parse(TESTFN)
168        with open(TESTFN, 'rb') as f:
169            self.check_parse(f)
170        # accept data with declared encoding
171        self.check_parse(BytesIO(xml_bytes(self.data, 'iso-8859-1')))
172        make_xml_file(self.data, 'iso-8859-1')
173        self.check_parse(TESTFN)
174        with open(TESTFN, 'rb') as f:
175            self.check_parse(f)
176        # fail on non-UTF-8 incompatible data without declared encoding
177        with self.assertRaises(SAXException):
178            self.check_parse(BytesIO(xml_bytes(self.data, 'iso-8859-1', None)))
179        make_xml_file(self.data, 'iso-8859-1', None)
180        with self.assertRaises(SAXException):
181            self.check_parse(TESTFN)
182        with open(TESTFN, 'rb') as f:
183            with self.assertRaises(SAXException):
184                self.check_parse(f)
185
186    def test_parse_path_object(self):
187        make_xml_file(self.data, 'utf-8', None)
188        self.check_parse(FakePath(TESTFN))
189
190    def test_parse_InputSource(self):
191        # accept data without declared but with explicitly specified encoding
192        make_xml_file(self.data, 'iso-8859-1', None)
193        with open(TESTFN, 'rb') as f:
194            input = InputSource()
195            input.setByteStream(f)
196            input.setEncoding('iso-8859-1')
197            self.check_parse(input)
198
199    def test_parse_close_source(self):
200        builtin_open = open
201        fileobj = None
202
203        def mock_open(*args):
204            nonlocal fileobj
205            fileobj = builtin_open(*args)
206            return fileobj
207
208        with mock.patch('xml.sax.saxutils.open', side_effect=mock_open):
209            make_xml_file(self.data, 'iso-8859-1', None)
210            with self.assertRaises(SAXException):
211                self.check_parse(TESTFN)
212            self.assertTrue(fileobj.closed)
213
214    def check_parseString(self, s):
215        from xml.sax import parseString
216        result = StringIO()
217        parseString(s, XMLGenerator(result, 'utf-8'))
218        self.assertEqual(result.getvalue(), xml_str(self.data, 'utf-8'))
219
220    def test_parseString_text(self):
221        encodings = ('us-ascii', 'iso-8859-1', 'utf-8',
222                     'utf-16', 'utf-16le', 'utf-16be')
223        for encoding in encodings:
224            self.check_parseString(xml_str(self.data, encoding))
225        self.check_parseString(self.data)
226
227    def test_parseString_bytes(self):
228        # UTF-8 is default encoding, US-ASCII is compatible with UTF-8,
229        # UTF-16 is autodetected
230        encodings = ('us-ascii', 'utf-8', 'utf-16', 'utf-16le', 'utf-16be')
231        for encoding in encodings:
232            self.check_parseString(xml_bytes(self.data, encoding))
233            self.check_parseString(xml_bytes(self.data, encoding, None))
234        # accept UTF-8 with BOM
235        self.check_parseString(xml_bytes(self.data, 'utf-8-sig', 'utf-8'))
236        self.check_parseString(xml_bytes(self.data, 'utf-8-sig', None))
237        # accept data with declared encoding
238        self.check_parseString(xml_bytes(self.data, 'iso-8859-1'))
239        # fail on non-UTF-8 incompatible data without declared encoding
240        with self.assertRaises(SAXException):
241            self.check_parseString(xml_bytes(self.data, 'iso-8859-1', None))
242
243class MakeParserTest(unittest.TestCase):
244    def test_make_parser2(self):
245        # Creating parsers several times in a row should succeed.
246        # Testing this because there have been failures of this kind
247        # before.
248        from xml.sax import make_parser
249        p = make_parser()
250        from xml.sax import make_parser
251        p = make_parser()
252        from xml.sax import make_parser
253        p = make_parser()
254        from xml.sax import make_parser
255        p = make_parser()
256        from xml.sax import make_parser
257        p = make_parser()
258        from xml.sax import make_parser
259        p = make_parser()
260
261    def test_make_parser3(self):
262        # Testing that make_parser can handle different types of
263        # iterables.
264        make_parser(['module'])
265        make_parser(('module', ))
266        make_parser({'module'})
267        make_parser(frozenset({'module'}))
268        make_parser({'module': None})
269        make_parser(iter(['module']))
270
271    def test_make_parser4(self):
272        # Testing that make_parser can handle empty iterables.
273        make_parser([])
274        make_parser(tuple())
275        make_parser(set())
276        make_parser(frozenset())
277        make_parser({})
278        make_parser(iter([]))
279
280    def test_make_parser5(self):
281        # Testing that make_parser can handle iterables with more than
282        # one item.
283        make_parser(['module1', 'module2'])
284        make_parser(('module1', 'module2'))
285        make_parser({'module1', 'module2'})
286        make_parser(frozenset({'module1', 'module2'}))
287        make_parser({'module1': None, 'module2': None})
288        make_parser(iter(['module1', 'module2']))
289
290# ===========================================================================
291#
292#   saxutils tests
293#
294# ===========================================================================
295
296class SaxutilsTest(unittest.TestCase):
297    # ===== escape
298    def test_escape_basic(self):
299        self.assertEqual(escape("Donald Duck & Co"), "Donald Duck &amp; Co")
300
301    def test_escape_all(self):
302        self.assertEqual(escape("<Donald Duck & Co>"),
303                         "&lt;Donald Duck &amp; Co&gt;")
304
305    def test_escape_extra(self):
306        self.assertEqual(escape("Hei på deg", {"å" : "&aring;"}),
307                         "Hei p&aring; deg")
308
309    # ===== unescape
310    def test_unescape_basic(self):
311        self.assertEqual(unescape("Donald Duck &amp; Co"), "Donald Duck & Co")
312
313    def test_unescape_all(self):
314        self.assertEqual(unescape("&lt;Donald Duck &amp; Co&gt;"),
315                         "<Donald Duck & Co>")
316
317    def test_unescape_extra(self):
318        self.assertEqual(unescape("Hei på deg", {"å" : "&aring;"}),
319                         "Hei p&aring; deg")
320
321    def test_unescape_amp_extra(self):
322        self.assertEqual(unescape("&amp;foo;", {"&foo;": "splat"}), "&foo;")
323
324    # ===== quoteattr
325    def test_quoteattr_basic(self):
326        self.assertEqual(quoteattr("Donald Duck & Co"),
327                         '"Donald Duck &amp; Co"')
328
329    def test_single_quoteattr(self):
330        self.assertEqual(quoteattr('Includes "double" quotes'),
331                         '\'Includes "double" quotes\'')
332
333    def test_double_quoteattr(self):
334        self.assertEqual(quoteattr("Includes 'single' quotes"),
335                         "\"Includes 'single' quotes\"")
336
337    def test_single_double_quoteattr(self):
338        self.assertEqual(quoteattr("Includes 'single' and \"double\" quotes"),
339                         "\"Includes 'single' and &quot;double&quot; quotes\"")
340
341    # ===== make_parser
342    def test_make_parser(self):
343        # Creating a parser should succeed - it should fall back
344        # to the expatreader
345        p = make_parser(['xml.parsers.no_such_parser'])
346
347
348class PrepareInputSourceTest(unittest.TestCase):
349
350    def setUp(self):
351        self.file = support.TESTFN
352        with open(self.file, "w") as tmp:
353            tmp.write("This was read from a file.")
354
355    def tearDown(self):
356        support.unlink(self.file)
357
358    def make_byte_stream(self):
359        return BytesIO(b"This is a byte stream.")
360
361    def make_character_stream(self):
362        return StringIO("This is a character stream.")
363
364    def checkContent(self, stream, content):
365        self.assertIsNotNone(stream)
366        self.assertEqual(stream.read(), content)
367        stream.close()
368
369
370    def test_character_stream(self):
371        # If the source is an InputSource with a character stream, use it.
372        src = InputSource(self.file)
373        src.setCharacterStream(self.make_character_stream())
374        prep = prepare_input_source(src)
375        self.assertIsNone(prep.getByteStream())
376        self.checkContent(prep.getCharacterStream(),
377                          "This is a character stream.")
378
379    def test_byte_stream(self):
380        # If the source is an InputSource that does not have a character
381        # stream but does have a byte stream, use the byte stream.
382        src = InputSource(self.file)
383        src.setByteStream(self.make_byte_stream())
384        prep = prepare_input_source(src)
385        self.assertIsNone(prep.getCharacterStream())
386        self.checkContent(prep.getByteStream(),
387                          b"This is a byte stream.")
388
389    def test_system_id(self):
390        # If the source is an InputSource that has neither a character
391        # stream nor a byte stream, open the system ID.
392        src = InputSource(self.file)
393        prep = prepare_input_source(src)
394        self.assertIsNone(prep.getCharacterStream())
395        self.checkContent(prep.getByteStream(),
396                          b"This was read from a file.")
397
398    def test_string(self):
399        # If the source is a string, use it as a system ID and open it.
400        prep = prepare_input_source(self.file)
401        self.assertIsNone(prep.getCharacterStream())
402        self.checkContent(prep.getByteStream(),
403                          b"This was read from a file.")
404
405    def test_path_objects(self):
406        # If the source is a Path object, use it as a system ID and open it.
407        prep = prepare_input_source(FakePath(self.file))
408        self.assertIsNone(prep.getCharacterStream())
409        self.checkContent(prep.getByteStream(),
410                          b"This was read from a file.")
411
412    def test_binary_file(self):
413        # If the source is a binary file-like object, use it as a byte
414        # stream.
415        prep = prepare_input_source(self.make_byte_stream())
416        self.assertIsNone(prep.getCharacterStream())
417        self.checkContent(prep.getByteStream(),
418                          b"This is a byte stream.")
419
420    def test_text_file(self):
421        # If the source is a text file-like object, use it as a character
422        # stream.
423        prep = prepare_input_source(self.make_character_stream())
424        self.assertIsNone(prep.getByteStream())
425        self.checkContent(prep.getCharacterStream(),
426                          "This is a character stream.")
427
428
429# ===== XMLGenerator
430
431class XmlgenTest:
432    def test_xmlgen_basic(self):
433        result = self.ioclass()
434        gen = XMLGenerator(result)
435        gen.startDocument()
436        gen.startElement("doc", {})
437        gen.endElement("doc")
438        gen.endDocument()
439
440        self.assertEqual(result.getvalue(), self.xml("<doc></doc>"))
441
442    def test_xmlgen_basic_empty(self):
443        result = self.ioclass()
444        gen = XMLGenerator(result, short_empty_elements=True)
445        gen.startDocument()
446        gen.startElement("doc", {})
447        gen.endElement("doc")
448        gen.endDocument()
449
450        self.assertEqual(result.getvalue(), self.xml("<doc/>"))
451
452    def test_xmlgen_content(self):
453        result = self.ioclass()
454        gen = XMLGenerator(result)
455
456        gen.startDocument()
457        gen.startElement("doc", {})
458        gen.characters("huhei")
459        gen.endElement("doc")
460        gen.endDocument()
461
462        self.assertEqual(result.getvalue(), self.xml("<doc>huhei</doc>"))
463
464    def test_xmlgen_content_empty(self):
465        result = self.ioclass()
466        gen = XMLGenerator(result, short_empty_elements=True)
467
468        gen.startDocument()
469        gen.startElement("doc", {})
470        gen.characters("huhei")
471        gen.endElement("doc")
472        gen.endDocument()
473
474        self.assertEqual(result.getvalue(), self.xml("<doc>huhei</doc>"))
475
476    def test_xmlgen_pi(self):
477        result = self.ioclass()
478        gen = XMLGenerator(result)
479
480        gen.startDocument()
481        gen.processingInstruction("test", "data")
482        gen.startElement("doc", {})
483        gen.endElement("doc")
484        gen.endDocument()
485
486        self.assertEqual(result.getvalue(),
487            self.xml("<?test data?><doc></doc>"))
488
489    def test_xmlgen_content_escape(self):
490        result = self.ioclass()
491        gen = XMLGenerator(result)
492
493        gen.startDocument()
494        gen.startElement("doc", {})
495        gen.characters("<huhei&")
496        gen.endElement("doc")
497        gen.endDocument()
498
499        self.assertEqual(result.getvalue(),
500            self.xml("<doc>&lt;huhei&amp;</doc>"))
501
502    def test_xmlgen_attr_escape(self):
503        result = self.ioclass()
504        gen = XMLGenerator(result)
505
506        gen.startDocument()
507        gen.startElement("doc", {"a": '"'})
508        gen.startElement("e", {"a": "'"})
509        gen.endElement("e")
510        gen.startElement("e", {"a": "'\""})
511        gen.endElement("e")
512        gen.startElement("e", {"a": "\n\r\t"})
513        gen.endElement("e")
514        gen.endElement("doc")
515        gen.endDocument()
516
517        self.assertEqual(result.getvalue(), self.xml(
518            "<doc a='\"'><e a=\"'\"></e>"
519            "<e a=\"'&quot;\"></e>"
520            "<e a=\"&#10;&#13;&#9;\"></e></doc>"))
521
522    def test_xmlgen_encoding(self):
523        encodings = ('iso-8859-15', 'utf-8', 'utf-8-sig',
524                     'utf-16', 'utf-16be', 'utf-16le',
525                     'utf-32', 'utf-32be', 'utf-32le')
526        for encoding in encodings:
527            result = self.ioclass()
528            gen = XMLGenerator(result, encoding=encoding)
529
530            gen.startDocument()
531            gen.startElement("doc", {"a": '\u20ac'})
532            gen.characters("\u20ac")
533            gen.endElement("doc")
534            gen.endDocument()
535
536            self.assertEqual(result.getvalue(),
537                self.xml('<doc a="\u20ac">\u20ac</doc>', encoding=encoding))
538
539    def test_xmlgen_unencodable(self):
540        result = self.ioclass()
541        gen = XMLGenerator(result, encoding='ascii')
542
543        gen.startDocument()
544        gen.startElement("doc", {"a": '\u20ac'})
545        gen.characters("\u20ac")
546        gen.endElement("doc")
547        gen.endDocument()
548
549        self.assertEqual(result.getvalue(),
550            self.xml('<doc a="&#8364;">&#8364;</doc>', encoding='ascii'))
551
552    def test_xmlgen_ignorable(self):
553        result = self.ioclass()
554        gen = XMLGenerator(result)
555
556        gen.startDocument()
557        gen.startElement("doc", {})
558        gen.ignorableWhitespace(" ")
559        gen.endElement("doc")
560        gen.endDocument()
561
562        self.assertEqual(result.getvalue(), self.xml("<doc> </doc>"))
563
564    def test_xmlgen_ignorable_empty(self):
565        result = self.ioclass()
566        gen = XMLGenerator(result, short_empty_elements=True)
567
568        gen.startDocument()
569        gen.startElement("doc", {})
570        gen.ignorableWhitespace(" ")
571        gen.endElement("doc")
572        gen.endDocument()
573
574        self.assertEqual(result.getvalue(), self.xml("<doc> </doc>"))
575
576    def test_xmlgen_encoding_bytes(self):
577        encodings = ('iso-8859-15', 'utf-8', 'utf-8-sig',
578                     'utf-16', 'utf-16be', 'utf-16le',
579                     'utf-32', 'utf-32be', 'utf-32le')
580        for encoding in encodings:
581            result = self.ioclass()
582            gen = XMLGenerator(result, encoding=encoding)
583
584            gen.startDocument()
585            gen.startElement("doc", {"a": '\u20ac'})
586            gen.characters("\u20ac".encode(encoding))
587            gen.ignorableWhitespace(" ".encode(encoding))
588            gen.endElement("doc")
589            gen.endDocument()
590
591            self.assertEqual(result.getvalue(),
592                self.xml('<doc a="\u20ac">\u20ac </doc>', encoding=encoding))
593
594    def test_xmlgen_ns(self):
595        result = self.ioclass()
596        gen = XMLGenerator(result)
597
598        gen.startDocument()
599        gen.startPrefixMapping("ns1", ns_uri)
600        gen.startElementNS((ns_uri, "doc"), "ns1:doc", {})
601        # add an unqualified name
602        gen.startElementNS((None, "udoc"), None, {})
603        gen.endElementNS((None, "udoc"), None)
604        gen.endElementNS((ns_uri, "doc"), "ns1:doc")
605        gen.endPrefixMapping("ns1")
606        gen.endDocument()
607
608        self.assertEqual(result.getvalue(), self.xml(
609           '<ns1:doc xmlns:ns1="%s"><udoc></udoc></ns1:doc>' %
610                                         ns_uri))
611
612    def test_xmlgen_ns_empty(self):
613        result = self.ioclass()
614        gen = XMLGenerator(result, short_empty_elements=True)
615
616        gen.startDocument()
617        gen.startPrefixMapping("ns1", ns_uri)
618        gen.startElementNS((ns_uri, "doc"), "ns1:doc", {})
619        # add an unqualified name
620        gen.startElementNS((None, "udoc"), None, {})
621        gen.endElementNS((None, "udoc"), None)
622        gen.endElementNS((ns_uri, "doc"), "ns1:doc")
623        gen.endPrefixMapping("ns1")
624        gen.endDocument()
625
626        self.assertEqual(result.getvalue(), self.xml(
627           '<ns1:doc xmlns:ns1="%s"><udoc/></ns1:doc>' %
628                                         ns_uri))
629
630    def test_1463026_1(self):
631        result = self.ioclass()
632        gen = XMLGenerator(result)
633
634        gen.startDocument()
635        gen.startElementNS((None, 'a'), 'a', {(None, 'b'):'c'})
636        gen.endElementNS((None, 'a'), 'a')
637        gen.endDocument()
638
639        self.assertEqual(result.getvalue(), self.xml('<a b="c"></a>'))
640
641    def test_1463026_1_empty(self):
642        result = self.ioclass()
643        gen = XMLGenerator(result, short_empty_elements=True)
644
645        gen.startDocument()
646        gen.startElementNS((None, 'a'), 'a', {(None, 'b'):'c'})
647        gen.endElementNS((None, 'a'), 'a')
648        gen.endDocument()
649
650        self.assertEqual(result.getvalue(), self.xml('<a b="c"/>'))
651
652    def test_1463026_2(self):
653        result = self.ioclass()
654        gen = XMLGenerator(result)
655
656        gen.startDocument()
657        gen.startPrefixMapping(None, 'qux')
658        gen.startElementNS(('qux', 'a'), 'a', {})
659        gen.endElementNS(('qux', 'a'), 'a')
660        gen.endPrefixMapping(None)
661        gen.endDocument()
662
663        self.assertEqual(result.getvalue(), self.xml('<a xmlns="qux"></a>'))
664
665    def test_1463026_2_empty(self):
666        result = self.ioclass()
667        gen = XMLGenerator(result, short_empty_elements=True)
668
669        gen.startDocument()
670        gen.startPrefixMapping(None, 'qux')
671        gen.startElementNS(('qux', 'a'), 'a', {})
672        gen.endElementNS(('qux', 'a'), 'a')
673        gen.endPrefixMapping(None)
674        gen.endDocument()
675
676        self.assertEqual(result.getvalue(), self.xml('<a xmlns="qux"/>'))
677
678    def test_1463026_3(self):
679        result = self.ioclass()
680        gen = XMLGenerator(result)
681
682        gen.startDocument()
683        gen.startPrefixMapping('my', 'qux')
684        gen.startElementNS(('qux', 'a'), 'a', {(None, 'b'):'c'})
685        gen.endElementNS(('qux', 'a'), 'a')
686        gen.endPrefixMapping('my')
687        gen.endDocument()
688
689        self.assertEqual(result.getvalue(),
690            self.xml('<my:a xmlns:my="qux" b="c"></my:a>'))
691
692    def test_1463026_3_empty(self):
693        result = self.ioclass()
694        gen = XMLGenerator(result, short_empty_elements=True)
695
696        gen.startDocument()
697        gen.startPrefixMapping('my', 'qux')
698        gen.startElementNS(('qux', 'a'), 'a', {(None, 'b'):'c'})
699        gen.endElementNS(('qux', 'a'), 'a')
700        gen.endPrefixMapping('my')
701        gen.endDocument()
702
703        self.assertEqual(result.getvalue(),
704            self.xml('<my:a xmlns:my="qux" b="c"/>'))
705
706    def test_5027_1(self):
707        # The xml prefix (as in xml:lang below) is reserved and bound by
708        # definition to http://www.w3.org/XML/1998/namespace.  XMLGenerator had
709        # a bug whereby a KeyError is raised because this namespace is missing
710        # from a dictionary.
711        #
712        # This test demonstrates the bug by parsing a document.
713        test_xml = StringIO(
714            '<?xml version="1.0"?>'
715            '<a:g1 xmlns:a="http://example.com/ns">'
716             '<a:g2 xml:lang="en">Hello</a:g2>'
717            '</a:g1>')
718
719        parser = make_parser()
720        parser.setFeature(feature_namespaces, True)
721        result = self.ioclass()
722        gen = XMLGenerator(result)
723        parser.setContentHandler(gen)
724        parser.parse(test_xml)
725
726        self.assertEqual(result.getvalue(),
727                         self.xml(
728                         '<a:g1 xmlns:a="http://example.com/ns">'
729                          '<a:g2 xml:lang="en">Hello</a:g2>'
730                         '</a:g1>'))
731
732    def test_5027_2(self):
733        # The xml prefix (as in xml:lang below) is reserved and bound by
734        # definition to http://www.w3.org/XML/1998/namespace.  XMLGenerator had
735        # a bug whereby a KeyError is raised because this namespace is missing
736        # from a dictionary.
737        #
738        # This test demonstrates the bug by direct manipulation of the
739        # XMLGenerator.
740        result = self.ioclass()
741        gen = XMLGenerator(result)
742
743        gen.startDocument()
744        gen.startPrefixMapping('a', 'http://example.com/ns')
745        gen.startElementNS(('http://example.com/ns', 'g1'), 'g1', {})
746        lang_attr = {('http://www.w3.org/XML/1998/namespace', 'lang'): 'en'}
747        gen.startElementNS(('http://example.com/ns', 'g2'), 'g2', lang_attr)
748        gen.characters('Hello')
749        gen.endElementNS(('http://example.com/ns', 'g2'), 'g2')
750        gen.endElementNS(('http://example.com/ns', 'g1'), 'g1')
751        gen.endPrefixMapping('a')
752        gen.endDocument()
753
754        self.assertEqual(result.getvalue(),
755                         self.xml(
756                         '<a:g1 xmlns:a="http://example.com/ns">'
757                          '<a:g2 xml:lang="en">Hello</a:g2>'
758                         '</a:g1>'))
759
760    def test_no_close_file(self):
761        result = self.ioclass()
762        def func(out):
763            gen = XMLGenerator(out)
764            gen.startDocument()
765            gen.startElement("doc", {})
766        func(result)
767        self.assertFalse(result.closed)
768
769    def test_xmlgen_fragment(self):
770        result = self.ioclass()
771        gen = XMLGenerator(result)
772
773        # Don't call gen.startDocument()
774        gen.startElement("foo", {"a": "1.0"})
775        gen.characters("Hello")
776        gen.endElement("foo")
777        gen.startElement("bar", {"b": "2.0"})
778        gen.endElement("bar")
779        # Don't call gen.endDocument()
780
781        self.assertEqual(result.getvalue(),
782            self.xml('<foo a="1.0">Hello</foo><bar b="2.0"></bar>')[len(self.xml('')):])
783
784class StringXmlgenTest(XmlgenTest, unittest.TestCase):
785    ioclass = StringIO
786
787    def xml(self, doc, encoding='iso-8859-1'):
788        return '<?xml version="1.0" encoding="%s"?>\n%s' % (encoding, doc)
789
790    test_xmlgen_unencodable = None
791
792class BytesXmlgenTest(XmlgenTest, unittest.TestCase):
793    ioclass = BytesIO
794
795    def xml(self, doc, encoding='iso-8859-1'):
796        return ('<?xml version="1.0" encoding="%s"?>\n%s' %
797                (encoding, doc)).encode(encoding, 'xmlcharrefreplace')
798
799class WriterXmlgenTest(BytesXmlgenTest):
800    class ioclass(list):
801        write = list.append
802        closed = False
803
804        def seekable(self):
805            return True
806
807        def tell(self):
808            # return 0 at start and not 0 after start
809            return len(self)
810
811        def getvalue(self):
812            return b''.join(self)
813
814class StreamWriterXmlgenTest(XmlgenTest, unittest.TestCase):
815    def ioclass(self):
816        raw = BytesIO()
817        writer = codecs.getwriter('ascii')(raw, 'xmlcharrefreplace')
818        writer.getvalue = raw.getvalue
819        return writer
820
821    def xml(self, doc, encoding='iso-8859-1'):
822        return ('<?xml version="1.0" encoding="%s"?>\n%s' %
823                (encoding, doc)).encode('ascii', 'xmlcharrefreplace')
824
825class StreamReaderWriterXmlgenTest(XmlgenTest, unittest.TestCase):
826    fname = support.TESTFN + '-codecs'
827
828    def ioclass(self):
829        writer = codecs.open(self.fname, 'w', encoding='ascii',
830                             errors='xmlcharrefreplace', buffering=0)
831        def cleanup():
832            writer.close()
833            support.unlink(self.fname)
834        self.addCleanup(cleanup)
835        def getvalue():
836            # Windows will not let use reopen without first closing
837            writer.close()
838            with open(writer.name, 'rb') as f:
839                return f.read()
840        writer.getvalue = getvalue
841        return writer
842
843    def xml(self, doc, encoding='iso-8859-1'):
844        return ('<?xml version="1.0" encoding="%s"?>\n%s' %
845                (encoding, doc)).encode('ascii', 'xmlcharrefreplace')
846
847start = b'<?xml version="1.0" encoding="iso-8859-1"?>\n'
848
849
850class XMLFilterBaseTest(unittest.TestCase):
851    def test_filter_basic(self):
852        result = BytesIO()
853        gen = XMLGenerator(result)
854        filter = XMLFilterBase()
855        filter.setContentHandler(gen)
856
857        filter.startDocument()
858        filter.startElement("doc", {})
859        filter.characters("content")
860        filter.ignorableWhitespace(" ")
861        filter.endElement("doc")
862        filter.endDocument()
863
864        self.assertEqual(result.getvalue(), start + b"<doc>content </doc>")
865
866# ===========================================================================
867#
868#   expatreader tests
869#
870# ===========================================================================
871
872with open(TEST_XMLFILE_OUT, 'rb') as f:
873    xml_test_out = f.read()
874
875class ExpatReaderTest(XmlTestBase):
876
877    # ===== XMLReader support
878
879    def test_expat_binary_file(self):
880        parser = create_parser()
881        result = BytesIO()
882        xmlgen = XMLGenerator(result)
883
884        parser.setContentHandler(xmlgen)
885        with open(TEST_XMLFILE, 'rb') as f:
886            parser.parse(f)
887
888        self.assertEqual(result.getvalue(), xml_test_out)
889
890    def test_expat_text_file(self):
891        parser = create_parser()
892        result = BytesIO()
893        xmlgen = XMLGenerator(result)
894
895        parser.setContentHandler(xmlgen)
896        with open(TEST_XMLFILE, 'rt', encoding='iso-8859-1') as f:
897            parser.parse(f)
898
899        self.assertEqual(result.getvalue(), xml_test_out)
900
901    @requires_nonascii_filenames
902    def test_expat_binary_file_nonascii(self):
903        fname = support.TESTFN_UNICODE
904        shutil.copyfile(TEST_XMLFILE, fname)
905        self.addCleanup(support.unlink, fname)
906
907        parser = create_parser()
908        result = BytesIO()
909        xmlgen = XMLGenerator(result)
910
911        parser.setContentHandler(xmlgen)
912        parser.parse(open(fname, 'rb'))
913
914        self.assertEqual(result.getvalue(), xml_test_out)
915
916    def test_expat_binary_file_bytes_name(self):
917        fname = os.fsencode(TEST_XMLFILE)
918        parser = create_parser()
919        result = BytesIO()
920        xmlgen = XMLGenerator(result)
921
922        parser.setContentHandler(xmlgen)
923        with open(fname, 'rb') as f:
924            parser.parse(f)
925
926        self.assertEqual(result.getvalue(), xml_test_out)
927
928    def test_expat_binary_file_int_name(self):
929        parser = create_parser()
930        result = BytesIO()
931        xmlgen = XMLGenerator(result)
932
933        parser.setContentHandler(xmlgen)
934        with open(TEST_XMLFILE, 'rb') as f:
935            with open(f.fileno(), 'rb', closefd=False) as f2:
936                parser.parse(f2)
937
938        self.assertEqual(result.getvalue(), xml_test_out)
939
940    # ===== DTDHandler support
941
942    class TestDTDHandler:
943
944        def __init__(self):
945            self._notations = []
946            self._entities  = []
947
948        def notationDecl(self, name, publicId, systemId):
949            self._notations.append((name, publicId, systemId))
950
951        def unparsedEntityDecl(self, name, publicId, systemId, ndata):
952            self._entities.append((name, publicId, systemId, ndata))
953
954
955    class TestEntityRecorder:
956        def __init__(self):
957            self.entities = []
958
959        def resolveEntity(self, publicId, systemId):
960            self.entities.append((publicId, systemId))
961            source = InputSource()
962            source.setPublicId(publicId)
963            source.setSystemId(systemId)
964            return source
965
966    def test_expat_dtdhandler(self):
967        parser = create_parser()
968        handler = self.TestDTDHandler()
969        parser.setDTDHandler(handler)
970
971        parser.feed('<!DOCTYPE doc [\n')
972        parser.feed('  <!ENTITY img SYSTEM "expat.gif" NDATA GIF>\n')
973        parser.feed('  <!NOTATION GIF PUBLIC "-//CompuServe//NOTATION Graphics Interchange Format 89a//EN">\n')
974        parser.feed(']>\n')
975        parser.feed('<doc></doc>')
976        parser.close()
977
978        self.assertEqual(handler._notations,
979            [("GIF", "-//CompuServe//NOTATION Graphics Interchange Format 89a//EN", None)])
980        self.assertEqual(handler._entities, [("img", None, "expat.gif", "GIF")])
981
982    def test_expat_external_dtd_enabled(self):
983        # clear _opener global variable
984        self.addCleanup(urllib.request.urlcleanup)
985
986        parser = create_parser()
987        parser.setFeature(feature_external_ges, True)
988        resolver = self.TestEntityRecorder()
989        parser.setEntityResolver(resolver)
990
991        with self.assertRaises(URLError):
992            parser.feed(
993                '<!DOCTYPE external SYSTEM "unsupported://non-existing">\n'
994            )
995        self.assertEqual(
996            resolver.entities, [(None, 'unsupported://non-existing')]
997        )
998
999    def test_expat_external_dtd_default(self):
1000        parser = create_parser()
1001        resolver = self.TestEntityRecorder()
1002        parser.setEntityResolver(resolver)
1003
1004        parser.feed(
1005            '<!DOCTYPE external SYSTEM "unsupported://non-existing">\n'
1006        )
1007        parser.feed('<doc />')
1008        parser.close()
1009        self.assertEqual(resolver.entities, [])
1010
1011    # ===== EntityResolver support
1012
1013    class TestEntityResolver:
1014
1015        def resolveEntity(self, publicId, systemId):
1016            inpsrc = InputSource()
1017            inpsrc.setByteStream(BytesIO(b"<entity/>"))
1018            return inpsrc
1019
1020    def test_expat_entityresolver_enabled(self):
1021        parser = create_parser()
1022        parser.setFeature(feature_external_ges, True)
1023        parser.setEntityResolver(self.TestEntityResolver())
1024        result = BytesIO()
1025        parser.setContentHandler(XMLGenerator(result))
1026
1027        parser.feed('<!DOCTYPE doc [\n')
1028        parser.feed('  <!ENTITY test SYSTEM "whatever">\n')
1029        parser.feed(']>\n')
1030        parser.feed('<doc>&test;</doc>')
1031        parser.close()
1032
1033        self.assertEqual(result.getvalue(), start +
1034                         b"<doc><entity></entity></doc>")
1035
1036    def test_expat_entityresolver_default(self):
1037        parser = create_parser()
1038        self.assertEqual(parser.getFeature(feature_external_ges), False)
1039        parser.setEntityResolver(self.TestEntityResolver())
1040        result = BytesIO()
1041        parser.setContentHandler(XMLGenerator(result))
1042
1043        parser.feed('<!DOCTYPE doc [\n')
1044        parser.feed('  <!ENTITY test SYSTEM "whatever">\n')
1045        parser.feed(']>\n')
1046        parser.feed('<doc>&test;</doc>')
1047        parser.close()
1048
1049        self.assertEqual(result.getvalue(), start +
1050                         b"<doc></doc>")
1051
1052    # ===== Attributes support
1053
1054    class AttrGatherer(ContentHandler):
1055
1056        def startElement(self, name, attrs):
1057            self._attrs = attrs
1058
1059        def startElementNS(self, name, qname, attrs):
1060            self._attrs = attrs
1061
1062    def test_expat_attrs_empty(self):
1063        parser = create_parser()
1064        gather = self.AttrGatherer()
1065        parser.setContentHandler(gather)
1066
1067        parser.feed("<doc/>")
1068        parser.close()
1069
1070        self.verify_empty_attrs(gather._attrs)
1071
1072    def test_expat_attrs_wattr(self):
1073        parser = create_parser()
1074        gather = self.AttrGatherer()
1075        parser.setContentHandler(gather)
1076
1077        parser.feed("<doc attr='val'/>")
1078        parser.close()
1079
1080        self.verify_attrs_wattr(gather._attrs)
1081
1082    def test_expat_nsattrs_empty(self):
1083        parser = create_parser(1)
1084        gather = self.AttrGatherer()
1085        parser.setContentHandler(gather)
1086
1087        parser.feed("<doc/>")
1088        parser.close()
1089
1090        self.verify_empty_nsattrs(gather._attrs)
1091
1092    def test_expat_nsattrs_wattr(self):
1093        parser = create_parser(1)
1094        gather = self.AttrGatherer()
1095        parser.setContentHandler(gather)
1096
1097        parser.feed("<doc xmlns:ns='%s' ns:attr='val'/>" % ns_uri)
1098        parser.close()
1099
1100        attrs = gather._attrs
1101
1102        self.assertEqual(attrs.getLength(), 1)
1103        self.assertEqual(attrs.getNames(), [(ns_uri, "attr")])
1104        self.assertTrue((attrs.getQNames() == [] or
1105                         attrs.getQNames() == ["ns:attr"]))
1106        self.assertEqual(len(attrs), 1)
1107        self.assertIn((ns_uri, "attr"), attrs)
1108        self.assertEqual(attrs.get((ns_uri, "attr")), "val")
1109        self.assertEqual(attrs.get((ns_uri, "attr"), 25), "val")
1110        self.assertEqual(list(attrs.items()), [((ns_uri, "attr"), "val")])
1111        self.assertEqual(list(attrs.values()), ["val"])
1112        self.assertEqual(attrs.getValue((ns_uri, "attr")), "val")
1113        self.assertEqual(attrs[(ns_uri, "attr")], "val")
1114
1115    # ===== InputSource support
1116
1117    def test_expat_inpsource_filename(self):
1118        parser = create_parser()
1119        result = BytesIO()
1120        xmlgen = XMLGenerator(result)
1121
1122        parser.setContentHandler(xmlgen)
1123        parser.parse(TEST_XMLFILE)
1124
1125        self.assertEqual(result.getvalue(), xml_test_out)
1126
1127    def test_expat_inpsource_sysid(self):
1128        parser = create_parser()
1129        result = BytesIO()
1130        xmlgen = XMLGenerator(result)
1131
1132        parser.setContentHandler(xmlgen)
1133        parser.parse(InputSource(TEST_XMLFILE))
1134
1135        self.assertEqual(result.getvalue(), xml_test_out)
1136
1137    @requires_nonascii_filenames
1138    def test_expat_inpsource_sysid_nonascii(self):
1139        fname = support.TESTFN_UNICODE
1140        shutil.copyfile(TEST_XMLFILE, fname)
1141        self.addCleanup(support.unlink, fname)
1142
1143        parser = create_parser()
1144        result = BytesIO()
1145        xmlgen = XMLGenerator(result)
1146
1147        parser.setContentHandler(xmlgen)
1148        parser.parse(InputSource(fname))
1149
1150        self.assertEqual(result.getvalue(), xml_test_out)
1151
1152    def test_expat_inpsource_byte_stream(self):
1153        parser = create_parser()
1154        result = BytesIO()
1155        xmlgen = XMLGenerator(result)
1156
1157        parser.setContentHandler(xmlgen)
1158        inpsrc = InputSource()
1159        with open(TEST_XMLFILE, 'rb') as f:
1160            inpsrc.setByteStream(f)
1161            parser.parse(inpsrc)
1162
1163        self.assertEqual(result.getvalue(), xml_test_out)
1164
1165    def test_expat_inpsource_character_stream(self):
1166        parser = create_parser()
1167        result = BytesIO()
1168        xmlgen = XMLGenerator(result)
1169
1170        parser.setContentHandler(xmlgen)
1171        inpsrc = InputSource()
1172        with open(TEST_XMLFILE, 'rt', encoding='iso-8859-1') as f:
1173            inpsrc.setCharacterStream(f)
1174            parser.parse(inpsrc)
1175
1176        self.assertEqual(result.getvalue(), xml_test_out)
1177
1178    # ===== IncrementalParser support
1179
1180    def test_expat_incremental(self):
1181        result = BytesIO()
1182        xmlgen = XMLGenerator(result)
1183        parser = create_parser()
1184        parser.setContentHandler(xmlgen)
1185
1186        parser.feed("<doc>")
1187        parser.feed("</doc>")
1188        parser.close()
1189
1190        self.assertEqual(result.getvalue(), start + b"<doc></doc>")
1191
1192    def test_expat_incremental_reset(self):
1193        result = BytesIO()
1194        xmlgen = XMLGenerator(result)
1195        parser = create_parser()
1196        parser.setContentHandler(xmlgen)
1197
1198        parser.feed("<doc>")
1199        parser.feed("text")
1200
1201        result = BytesIO()
1202        xmlgen = XMLGenerator(result)
1203        parser.setContentHandler(xmlgen)
1204        parser.reset()
1205
1206        parser.feed("<doc>")
1207        parser.feed("text")
1208        parser.feed("</doc>")
1209        parser.close()
1210
1211        self.assertEqual(result.getvalue(), start + b"<doc>text</doc>")
1212
1213    # ===== Locator support
1214
1215    def test_expat_locator_noinfo(self):
1216        result = BytesIO()
1217        xmlgen = XMLGenerator(result)
1218        parser = create_parser()
1219        parser.setContentHandler(xmlgen)
1220
1221        parser.feed("<doc>")
1222        parser.feed("</doc>")
1223        parser.close()
1224
1225        self.assertEqual(parser.getSystemId(), None)
1226        self.assertEqual(parser.getPublicId(), None)
1227        self.assertEqual(parser.getLineNumber(), 1)
1228
1229    def test_expat_locator_withinfo(self):
1230        result = BytesIO()
1231        xmlgen = XMLGenerator(result)
1232        parser = create_parser()
1233        parser.setContentHandler(xmlgen)
1234        parser.parse(TEST_XMLFILE)
1235
1236        self.assertEqual(parser.getSystemId(), TEST_XMLFILE)
1237        self.assertEqual(parser.getPublicId(), None)
1238
1239    @requires_nonascii_filenames
1240    def test_expat_locator_withinfo_nonascii(self):
1241        fname = support.TESTFN_UNICODE
1242        shutil.copyfile(TEST_XMLFILE, fname)
1243        self.addCleanup(support.unlink, fname)
1244
1245        result = BytesIO()
1246        xmlgen = XMLGenerator(result)
1247        parser = create_parser()
1248        parser.setContentHandler(xmlgen)
1249        parser.parse(fname)
1250
1251        self.assertEqual(parser.getSystemId(), fname)
1252        self.assertEqual(parser.getPublicId(), None)
1253
1254
1255# ===========================================================================
1256#
1257#   error reporting
1258#
1259# ===========================================================================
1260
1261class ErrorReportingTest(unittest.TestCase):
1262    def test_expat_inpsource_location(self):
1263        parser = create_parser()
1264        parser.setContentHandler(ContentHandler()) # do nothing
1265        source = InputSource()
1266        source.setByteStream(BytesIO(b"<foo bar foobar>"))   #ill-formed
1267        name = "a file name"
1268        source.setSystemId(name)
1269        try:
1270            parser.parse(source)
1271            self.fail()
1272        except SAXException as e:
1273            self.assertEqual(e.getSystemId(), name)
1274
1275    def test_expat_incomplete(self):
1276        parser = create_parser()
1277        parser.setContentHandler(ContentHandler()) # do nothing
1278        self.assertRaises(SAXParseException, parser.parse, StringIO("<foo>"))
1279        self.assertEqual(parser.getColumnNumber(), 5)
1280        self.assertEqual(parser.getLineNumber(), 1)
1281
1282    def test_sax_parse_exception_str(self):
1283        # pass various values from a locator to the SAXParseException to
1284        # make sure that the __str__() doesn't fall apart when None is
1285        # passed instead of an integer line and column number
1286        #
1287        # use "normal" values for the locator:
1288        str(SAXParseException("message", None,
1289                              self.DummyLocator(1, 1)))
1290        # use None for the line number:
1291        str(SAXParseException("message", None,
1292                              self.DummyLocator(None, 1)))
1293        # use None for the column number:
1294        str(SAXParseException("message", None,
1295                              self.DummyLocator(1, None)))
1296        # use None for both:
1297        str(SAXParseException("message", None,
1298                              self.DummyLocator(None, None)))
1299
1300    class DummyLocator:
1301        def __init__(self, lineno, colno):
1302            self._lineno = lineno
1303            self._colno = colno
1304
1305        def getPublicId(self):
1306            return "pubid"
1307
1308        def getSystemId(self):
1309            return "sysid"
1310
1311        def getLineNumber(self):
1312            return self._lineno
1313
1314        def getColumnNumber(self):
1315            return self._colno
1316
1317# ===========================================================================
1318#
1319#   xmlreader tests
1320#
1321# ===========================================================================
1322
1323class XmlReaderTest(XmlTestBase):
1324
1325    # ===== AttributesImpl
1326    def test_attrs_empty(self):
1327        self.verify_empty_attrs(AttributesImpl({}))
1328
1329    def test_attrs_wattr(self):
1330        self.verify_attrs_wattr(AttributesImpl({"attr" : "val"}))
1331
1332    def test_nsattrs_empty(self):
1333        self.verify_empty_nsattrs(AttributesNSImpl({}, {}))
1334
1335    def test_nsattrs_wattr(self):
1336        attrs = AttributesNSImpl({(ns_uri, "attr") : "val"},
1337                                 {(ns_uri, "attr") : "ns:attr"})
1338
1339        self.assertEqual(attrs.getLength(), 1)
1340        self.assertEqual(attrs.getNames(), [(ns_uri, "attr")])
1341        self.assertEqual(attrs.getQNames(), ["ns:attr"])
1342        self.assertEqual(len(attrs), 1)
1343        self.assertIn((ns_uri, "attr"), attrs)
1344        self.assertEqual(list(attrs.keys()), [(ns_uri, "attr")])
1345        self.assertEqual(attrs.get((ns_uri, "attr")), "val")
1346        self.assertEqual(attrs.get((ns_uri, "attr"), 25), "val")
1347        self.assertEqual(list(attrs.items()), [((ns_uri, "attr"), "val")])
1348        self.assertEqual(list(attrs.values()), ["val"])
1349        self.assertEqual(attrs.getValue((ns_uri, "attr")), "val")
1350        self.assertEqual(attrs.getValueByQName("ns:attr"), "val")
1351        self.assertEqual(attrs.getNameByQName("ns:attr"), (ns_uri, "attr"))
1352        self.assertEqual(attrs[(ns_uri, "attr")], "val")
1353        self.assertEqual(attrs.getQNameByName((ns_uri, "attr")), "ns:attr")
1354
1355
1356def test_main():
1357    run_unittest(MakeParserTest,
1358                 ParseTest,
1359                 SaxutilsTest,
1360                 PrepareInputSourceTest,
1361                 StringXmlgenTest,
1362                 BytesXmlgenTest,
1363                 WriterXmlgenTest,
1364                 StreamWriterXmlgenTest,
1365                 StreamReaderWriterXmlgenTest,
1366                 ExpatReaderTest,
1367                 ErrorReportingTest,
1368                 XmlReaderTest)
1369
1370if __name__ == "__main__":
1371    test_main()
1372