# !/usr/bin/env python
#
# Copyright (c), 2016-2020, SISSA (International School for Advanced Studies).
# All rights reserved.
# This file is distributed under the terms of the MIT License.
# See the file 'LICENSE' in the root directory of the present
# distribution, or http://opensource.org/licenses/MIT.
#
# @author Davide Brunato <brunato@sissa.it>
#
import unittest
import os
import base64
import json
import math
from decimal import Decimal
from collections.abc import MutableMapping, MutableSequence, Set
from textwrap import dedent
from xml.etree import ElementTree

try:
    import lxml.etree as lxml_etree
except ImportError:
    lxml_etree = None

from elementpath import datatypes
import xmlschema
from xmlschema import XMLSchemaValidationError, ParkerConverter, BadgerFishConverter, \
    AbderaConverter, JsonMLConverter, ColumnarConverter, ElementData

from xmlschema.names import XSD_STRING, XSI_NIL
from xmlschema.converters import UnorderedConverter
from xmlschema.validators import XMLSchema11, ModelVisitor
from xmlschema.testing import XsdValidatorTestCase, etree_elements_assert_equal

VEHICLES_DICT = {
    '@xmlns:vh': 'http://example.com/vehicles',
    '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
    '@xsi:schemaLocation': 'http://example.com/vehicles vehicles.xsd',
    'vh:cars': {
        'vh:car': [
            {'@make': 'Porsche', '@model': '911'},
            {'@make': 'Porsche', '@model': '911'}
        ]},
    'vh:bikes': {
        'vh:bike': [
            {'@make': 'Harley-Davidson', '@model': 'WL'},
            {'@make': 'Yamaha', '@model': 'XS650'}
        ]}
}

VEHICLES_DICT_ALT = [
    {'vh:cars': [
        {'vh:car': None, '@make': 'Porsche', '@model': '911'},
        {'vh:car': None, '@make': 'Porsche', '@model': '911'}
    ]},
    {'vh:bikes': [
        {'vh:bike': None, '@make': 'Harley-Davidson', '@model': 'WL'},
        {'vh:bike': None, '@make': 'Yamaha', '@model': 'XS650'}
    ]},
    {'@xsi:schemaLocation': 'http://example.com/vehicles vehicles.xsd'}
]

VEHICLES_DICT_OVERRIDE_PREFIX_1 = {
    '@xmlns:vh': 'http://example.com/vehicles',
    '@xmlns:vh2': 'http://example.com/vehicles',
    '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
    '@xsi:schemaLocation': 'http://example.com/vehicles vehicles.xsd',
    'vh2:cars': {
        'vh2:car': [
            {'@make': 'Porsche', '@model': '911'},
            {'@make': 'Porsche', '@model': '911'}
        ]},
    'vh2:bikes': {
        'vh2:bike': [
            {'@make': 'Harley-Davidson', '@model': 'WL'},
            {'@make': 'Yamaha', '@model': 'XS650'}
        ]}
}

VEHICLES_DICT_OVERRIDE_PREFIX_2 = {
    '@xmlns': 'http://example.com/vehicles',
    '@xmlns:vh': 'http://example.com/vehicles',
    '@xmlns:vh-alt': 'http://example.com/vehicles',
    '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
    '@xsi:schemaLocation': 'http://example.com/vehicles vehicles.xsd',
    'vh-alt:cars': {
        'vh-alt:car': [
            {'@make': 'Porsche', '@model': '911'},
            {'@make': 'Porsche', '@model': '911'}
        ]},
    'vh-alt:bikes': {
        'vh-alt:bike': [
            {'@make': 'Harley-Davidson', '@model': 'WL'},
            {'@make': 'Yamaha', '@model': 'XS650'}
        ]}
}

VEHICLES_DICT_OVERRIDE_PREFIX_3 = {
    '@xmlns': 'http://example.com/vehicles',
    '@xmlns:vh': 'http://example.com/vehicles',
    '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
    '@xsi:schemaLocation': 'http://example.com/vehicles vehicles.xsd',
    'cars': {
        'car': [
            {'@make': 'Porsche', '@model': '911'},
            {'@make': 'Porsche', '@model': '911'}
        ]},
    'bikes': {
        'bike': [
            {'@make': 'Harley-Davidson', '@model': 'WL'},
            {'@make': 'Yamaha', '@model': 'XS650'}
        ]}
}

COLLECTION_DICT = {
    '@xmlns:col': 'http://example.com/ns/collection',
    '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
    '@xsi:schemaLocation': 'http://example.com/ns/collection collection.xsd',
    'object': [{
        '@available': True,
        '@id': 'b0836217462',
        'author': {
            '@id': 'PAR',
            'born': '1841-02-25',
            'dead': '1919-12-03',
            'name': 'Pierre-Auguste Renoir',
            'qualification': 'painter'
        },
        'estimation': Decimal('10000.00'),
        'position': 1,
        'title': 'The Umbrellas',
        'year': '1886'},
        {
            '@available': True,
            '@id': 'b0836217463',
            'author': {
                '@id': 'JM',
                'born': '1893-04-20',
                'dead': '1983-12-25',
                'name': 'Joan Miró',
                'qualification': 'painter, sculptor and ceramicist'
            },
            'position': 2,
            'title': None,
            'year': '1925'
    }]
}

MENU_DICT = {
    '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
    '@xsi:noNamespaceSchemaLocation': 'menù.xsd',
    'antipasto': ['Affettati misti', 'Bruschetta', 'Polenta e funghi'],
    'primo': ['Lasagne', 'Gnocchi al ragù', 'Risotto allo zafferano'],
    'secondo': ['Tagliata di pollo', 'Cotoletta alla milanese', 'Caprese'],
    'dolce': ['Crostata ai mirtilli', 'Tiramisù']
}

COLLECTION_PARKER = {
    'object': [{'author': {'born': '1841-02-25',
                           'dead': '1919-12-03',
                           'name': 'Pierre-Auguste Renoir',
                           'qualification': 'painter'},
                'estimation': 10000.0,
                'position': 1,
                'title': 'The Umbrellas',
                'year': '1886'},
               {'author': {'born': '1893-04-20',
                           'dead': '1983-12-25',
                           'name': 'Joan Miró',
                           'qualification': 'painter, sculptor and ceramicist'},
                'position': 2,
                'title': None,
                'year': '1925'}]}

COLLECTION_PARKER_ROOT = {
    'col:collection': {'object': [{'author': {'born': '1841-02-25',
                                              'dead': '1919-12-03',
                                              'name': 'Pierre-Auguste Renoir',
                                              'qualification': 'painter'},
                                   'estimation': 10000.0,
                                   'position': 1,
                                   'title': 'The Umbrellas',
                                   'year': '1886'},
                                  {'author': {'born': '1893-04-20',
                                              'dead': '1983-12-25',
                                              'name': 'Joan Miró',
                                              'qualification': 'painter, sculptor and ceramicist'},
                                   'position': 2,
                                   'title': None,
                                   'year': '1925'}]}}

COLLECTION_BADGERFISH = {
    'col:collection': {
        '@xsi:schemaLocation': 'http://example.com/ns/collection collection.xsd',
        '@xmlns': {
            'col': 'http://example.com/ns/collection',
            'xsi': 'http://www.w3.org/2001/XMLSchema-instance'},
        'object': [{
            '@available': True,
            '@id': 'b0836217462',
            'author': {
                '@id': 'PAR',
                'born': {'$': '1841-02-25'},
                'dead': {'$': '1919-12-03'},
                'name': {'$': 'Pierre-Auguste Renoir'},
                'qualification': {'$': 'painter'}},
            'estimation': {'$': 10000.0},
            'position': {'$': 1},
            'title': {'$': 'The Umbrellas'},
            'year': {'$': '1886'}},
            {
                '@available': True,
                '@id': 'b0836217463',
                'author': {
                    '@id': 'JM',
                    'born': {'$': '1893-04-20'},
                    'dead': {'$': '1983-12-25'},
                    'name': {'$': 'Joan Miró'},
                    'qualification': {
                        '$': 'painter, sculptor and ceramicist'}
                },
                'position': {'$': 2},
                'title': {},
                'year': {'$': '1925'}
        }]
    }
}

COLLECTION_ABDERA = {
    'col:collection': {
        'attributes': {
            'xsi:schemaLocation': 'http://example.com/ns/collection collection.xsd'
        },
        'children': [
            {
                'object': [
                    {
                        'attributes': {'available': True, 'id': 'b0836217462'},
                        'children': [{
                            'author': {
                                'attributes': {'id': 'PAR'},
                                'children': [{
                                    'born': '1841-02-25',
                                    'dead': '1919-12-03',
                                    'name': 'Pierre-Auguste Renoir',
                                    'qualification': 'painter'}
                                ]},
                            'estimation': 10000.0,
                            'position': 1,
                            'title': 'The Umbrellas',
                            'year': '1886'}
                        ]},
                    {
                        'attributes': {'available': True, 'id': 'b0836217463'},
                        'children': [{
                            'author': {
                                'attributes': {'id': 'JM'},
                                'children': [{
                                    'born': '1893-04-20',
                                    'dead': '1983-12-25',
                                    'name': 'Joan Miró',
                                    'qualification': 'painter, sculptor and ceramicist'}
                                ]},
                            'position': 2,
                            'title': [],
                            'year': '1925'
                        }]
                    }]
            }
        ]}
}

COLLECTION_JSON_ML = [
    'col:collection',
    {'xmlns:col': 'http://example.com/ns/collection',
     'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
     'xsi:schemaLocation': 'http://example.com/ns/collection collection.xsd'},
    ['object',
     {'available': True, 'id': 'b0836217462'},
     ['position', 1],
     ['title', 'The Umbrellas'],
     ['year', '1886'],
     [
         'author',
         {'id': 'PAR'},
         ['name', 'Pierre-Auguste Renoir'],
         ['born', '1841-02-25'],
         ['dead', '1919-12-03'],
         ['qualification', 'painter']
     ],
     [
         'estimation',
         Decimal('10000.00')
     ]],
    ['object',
     {'available': True, 'id': 'b0836217463'},
     ['position', 2],
     ['title'],
     ['year', '1925'],
     [
         'author',
         {'id': 'JM'},
         ['name', 'Joan Miró'],
         ['born', '1893-04-20'],
         ['dead', '1983-12-25'],
         ['qualification', 'painter, sculptor and ceramicist']
     ]]
]

COLLECTION_COLUMNAR = {
    'collection': {
        'collectionxsi:schemaLocation': 'http://example.com/ns/collection collection.xsd',
        'object': [{
            'objectid': 'b0836217462',
            'objectavailable': True,
            'position': 1,
            'title': 'The Umbrellas',
            'year': '1886',
            'author': {
                'authorid': 'PAR',
                'name': 'Pierre-Auguste Renoir',
                'born': '1841-02-25',
                'dead': '1919-12-03',
                'qualification': 'painter'
            },
            'estimation': Decimal('10000.00')}, {
            'objectid': 'b0836217463',
            'objectavailable': True,
            'position': 2,
            'title': None,
            'year': '1925',
            'author': {
                'authorid': 'JM',
                'name': 'Joan Miró',
                'born': '1893-04-20',
                'dead': '1983-12-25',
                'qualification': 'painter, sculptor and ceramicist'
            }
        }]
    }
}

COLLECTION_COLUMNAR_ = {
    'collection': {
        'collection_xsi:schemaLocation': 'http://example.com/ns/collection collection.xsd',
        'object': [{
            'object_id': 'b0836217462',
            'object_available': True,
            'position': 1,
            'title': 'The Umbrellas',
            'year': '1886',
            'author': {
                'author_id': 'PAR',
                'name': 'Pierre-Auguste Renoir',
                'born': '1841-02-25',
                'dead': '1919-12-03',
                'qualification': 'painter'
            },
            'estimation': Decimal('10000.00')}, {
            'object_id': 'b0836217463',
            'object_available': True,
            'position': 2,
            'title': None,
            'year': '1925',
            'author': {
                'author_id': 'JM',
                'name': 'Joan Miró',
                'born': '1893-04-20',
                'dead': '1983-12-25',
                'qualification': 'painter, sculptor and ceramicist'
            }
        }]
    }
}

COLLECTION_XMLNS_PROCESSING_STACKED = {
    '@xmlns:col1': 'http://example.com/ns/collection',
    '@xmlns:col': 'http://xmlschema.test/ns',
    '@xmlns': 'http://xmlschema.test/ns',
    '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
    '@xsi:schemaLocation': 'http://example.com/ns/collection collection5.xsd',
    'col:object': [
        {'@xmlns:col': 'http://example.com/ns/collection',
         '@id': 'b0836217462',
         '@available': True,
         'col:position': 1,
         'col:title': 'The Umbrellas',
         'col:year': '1886',
         'col:author': {
             '@id': 'PAR',
             'col:name': 'Pierre-Auguste Renoir',
             'col:born': '1841-02-25',
             'col:dead': '1919-12-03',
             'col:qualification': 'painter'
         },
         'col:estimation': Decimal('10000.00')}],
    'object': [
        {'@xmlns': 'http://example.com/ns/collection',
         '@id': 'b0836217463',
         '@available': True,
         'position': 2,
         'title': None,
         'year': '1925',
         'author': {
            '@id': 'JM',
            'name': 'Joan Miró',
            'born': '1893-04-20',
            'dead': '1983-12-25',
            'qualification': 'painter, sculptor and ceramicist'
         }}
    ]
}

COLLECTION_XMLNS_PROCESSING_COLLAPSED = {
    '@xmlns': 'http://xmlschema.test/ns',
    '@xmlns:col': 'http://xmlschema.test/ns',
    '@xmlns:col0': 'http://example.com/ns/collection',
    '@xmlns:col1': 'http://example.com/ns/collection',
    '@xmlns:default': 'http://example.com/ns/collection',
    '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
    '@xsi:schemaLocation': 'http://example.com/ns/collection collection5.xsd',
    'col1:object': [
        {'@id': 'b0836217462',
         '@available': True,
         'col1:position': 1,
         'col1:title': 'The Umbrellas',
         'col1:year': '1886',
         'col1:author': {
             '@id': 'PAR',
             'col1:name': 'Pierre-Auguste Renoir',
             'col1:born': '1841-02-25',
             'col1:dead': '1919-12-03',
             'col1:qualification': 'painter'
         },
         'col1:estimation': Decimal('10000.00')},
        {'@id': 'b0836217463',
         '@available': True,
         'col1:position': 2,
         'col1:title': None,
         'col1:year': '1925',
         'col1:author': {
            '@id': 'JM',
            'col1:name': 'Joan Miró',
            'col1:born': '1893-04-20',
            'col1:dead': '1983-12-25',
            'col1:qualification': 'painter, sculptor and ceramicist'
         }}
    ]
}

COLLECTION_XMLNS_PROCESSING_ROOT_ONLY = {
    '@xmlns': 'http://xmlschema.test/ns',
    '@xmlns:col': 'http://xmlschema.test/ns',
    '@xmlns:col1': 'http://example.com/ns/collection',
    '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
    '@xsi:schemaLocation': 'http://example.com/ns/collection collection5.xsd',
    'col1:object': [
        {'@id': 'b0836217462',
         '@available': True,
         'col1:position': 1,
         'col1:title': 'The Umbrellas',
         'col1:year': '1886',
         'col1:author': {
             '@id': 'PAR',
             'col1:name': 'Pierre-Auguste Renoir',
             'col1:born': '1841-02-25',
             'col1:dead': '1919-12-03',
             'col1:qualification': 'painter'
         },
         'col1:estimation': Decimal('10000.00')},
        {'@id': 'b0836217463',
         '@available': True,
         'col1:position': 2,
         'col1:title': None,
         'col1:year': '1925',
         'col1:author': {
            '@id': 'JM',
            'col1:name': 'Joan Miró',
            'col1:born': '1893-04-20',
            'col1:dead': '1983-12-25',
            'col1:qualification': 'painter, sculptor and ceramicist'
         }}
    ]
}


COLLECTION_XMLNS_PROCESSING_NONE = {
    '@xmlns:col-alt': 'http://example.com/ns/collection',
    '@xmlns:xsi-alt': 'http://www.w3.org/2001/XMLSchema-instance',
    '@xsi-alt:schemaLocation': 'http://example.com/ns/collection collection5.xsd',
    'col-alt:object': [
        {'@id': 'b0836217462',
         '@available': True,
         'col-alt:position': 1,
         'col-alt:title': 'The Umbrellas',
         'col-alt:year': '1886',
         'col-alt:author': {
             '@id': 'PAR',
             'col-alt:name': 'Pierre-Auguste Renoir',
             'col-alt:born': '1841-02-25',
             'col-alt:dead': '1919-12-03',
             'col-alt:qualification': 'painter'
         },
         'col-alt:estimation': Decimal('10000.00')},
        {'@id': 'b0836217463',
         '@available': True,
         'col-alt:position': 2,
         'col-alt:title': None,
         'col-alt:year': '1925',
         'col-alt:author': {
            '@id': 'JM',
            'col-alt:name': 'Joan Miró',
            'col-alt:born': '1893-04-20',
            'col-alt:dead': '1983-12-25',
            'col-alt:qualification': 'painter, sculptor and ceramicist'
         }}
    ]
}

DATA_DICT = {
    '@xmlns:ns': 'ns',
    '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
    '@xsi:schemaLocation': 'ns ./simple-types.xsd',
    'certification': [
        {'$': 'ISO-9001', '@Year': 1999},
        {'$': 'ISO-27001', '@Year': 2009}
    ],
    'decimal_value': [Decimal('1')],
    'hexbin': 'AABBCCDD',
    'menù': 'baccalà mantecato',
    'qname': 'ns:foo',
    'complex_boolean': [
        {'$': True, '@Type': 2}, {'$': False, '@Type': 1}, True, False
    ],
    'simple_boolean': [True, False],
    'date_and_time': '2020-03-05T23:04:10.047',  # xs:dateTime is not decoded for default
    'list_of_floats': [float('inf'), float('-inf')],
}


def iter_nested_iterables(obj):
    if not isinstance(obj, (MutableMapping, MutableSequence, Set, tuple)):
        yield obj
    else:
        for item in obj.values() if isinstance(obj, MutableMapping) else obj:
            if not isinstance(item, (MutableMapping, MutableSequence, Set, tuple)):
                yield item
            else:
                yield from iter_nested_iterables(item)


class TestDecoding(XsdValidatorTestCase):
    TEST_CASES_DIR = os.path.join(os.path.dirname(__file__), '../test_cases')

    def check_decode(self, xsd_component, data, expected, **kwargs):
        if isinstance(expected, type) and issubclass(expected, Exception):
            self.assertRaises(expected, xsd_component.decode, data, **kwargs)
        else:
            obj = xsd_component.decode(data, **kwargs)
            if isinstance(obj, tuple) and len(obj) == 2 and isinstance(obj[1], list) \
                    and isinstance(obj[1][0], Exception):
                self.assertEqual(expected, obj[0])
                self.assertTrue(isinstance(obj[0], type(expected)))
            else:
                self.assertEqual(expected, obj)
                self.assertTrue(isinstance(obj, type(expected)))

    @unittest.skipIf(lxml_etree is None, "The lxml library is not available.")
    def test_lxml(self):
        vh_xml_tree = lxml_etree.parse(self.vh_xml_file)
        self.assertEqual(self.vh_schema.to_dict(vh_xml_tree), VEHICLES_DICT)
        self.assertEqual(xmlschema.to_dict(vh_xml_tree, self.vh_schema.url), VEHICLES_DICT)

    def test_to_dict_from_etree(self):
        vh_xml_tree = ElementTree.parse(self.vh_xml_file)
        col_xml_tree = ElementTree.parse(self.col_xml_file)

        xml_dict = self.vh_schema.to_dict(vh_xml_tree)
        self.assertNotEqual(xml_dict, VEHICLES_DICT)

        xml_dict = self.vh_schema.to_dict(vh_xml_tree, namespaces=self.vh_namespaces)
        self.assertEqual(xml_dict, VEHICLES_DICT)

        xml_dict = xmlschema.to_dict(
            vh_xml_tree, self.vh_schema.url, namespaces=self.vh_namespaces
        )
        self.assertEqual(xml_dict, VEHICLES_DICT)

        xml_dict = self.col_schema.to_dict(col_xml_tree)
        self.assertNotEqual(xml_dict, COLLECTION_DICT)

        xml_dict = self.col_schema.to_dict(col_xml_tree, namespaces=self.col_namespaces)
        self.assertEqual(xml_dict, COLLECTION_DICT)

        xml_dict = xmlschema.to_dict(col_xml_tree, self.col_schema.url,
                                     namespaces=self.col_namespaces)
        self.assertEqual(xml_dict, COLLECTION_DICT)

    def test_to_dict_from_string(self):
        with open(self.vh_xml_file) as f:
            vh_xml_string = f.read()

        with open(self.col_xml_file) as f:
            col_xml_string = f.read()

        xml_dict = self.vh_schema.to_dict(vh_xml_string, namespaces=self.vh_namespaces)
        self.assertEqual(xml_dict, VEHICLES_DICT)

        xml_dict = xmlschema.to_dict(vh_xml_string, self.vh_schema.url,
                                     namespaces=self.vh_namespaces)
        self.assertEqual(xml_dict, VEHICLES_DICT)

        xml_dict = self.col_schema.to_dict(col_xml_string, namespaces=self.col_namespaces)
        self.assertTrue(xml_dict, COLLECTION_DICT)

        xml_dict = xmlschema.to_dict(col_xml_string, self.col_schema.url,
                                     namespaces=self.col_namespaces)
        self.assertTrue(xml_dict, COLLECTION_DICT)

    def test_date_decoding(self):
        # Issue #136
        schema = self.schema_class("""<?xml version="1.0" encoding="utf-8"?>
            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" version="1.0">
                <xs:element name="Date">
                    <xs:simpleType>
                        <xs:restriction base="xs:date">
                            <xs:minInclusive value="2000-01-01"/>
                            <xs:maxInclusive value="2099-12-31"/>
                        </xs:restriction>
                    </xs:simpleType>
                </xs:element>
            </xs:schema>""")

        self.assertEqual(schema.to_dict("<Date>2019-01-01</Date>"), '2019-01-01')
        self.assertEqual(schema.to_dict("<Date>2019-01-01</Date>", datetime_types=True),
                         datatypes.Date10.fromstring('2019-01-01'))

        data, errors = schema.to_dict("<Date>2019-01-01</Date>", validation='lax')
        self.assertEqual(data, '2019-01-01')
        self.assertEqual(errors, [])

        data, errors = schema.to_dict("<Date>2019-01-01</Date>", validation='lax',
                                      datetime_types=True)
        self.assertEqual(data, datatypes.Date10.fromstring('2019-01-01'))
        self.assertEqual(errors, [])

        data, errors = schema.to_dict("<Date>1999-12-31</Date>", validation='lax')
        self.assertEqual(data, '1999-12-31')
        self.assertEqual(len(errors), 1)
        self.assertIn('value has to be greater or equal than', str(errors[0]))

        data, errors = schema.to_dict("<Date>1999-12-31</Date>", validation='lax',
                                      datetime_types=True)
        self.assertEqual(data, datatypes.Date10.fromstring('1999-12-31'))
        self.assertEqual(len(errors), 1)

        data, errors = schema.to_dict("<Date>2019</Date>", validation='lax')
        self.assertIsNone(data)
        self.assertEqual(len(errors), 1)

        with self.assertRaises(XMLSchemaValidationError):
            schema.to_dict("<Date>2019</Date>")

        data, errors = schema.to_dict("<Date>2019</Date>", validation='lax')
        self.assertIsNone(data)
        self.assertEqual(len(errors), 1)

    def test_qname_decoding(self):
        schema = self.schema_class("""<?xml version="1.0" encoding="utf-8"?>
            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
              <xs:element name="root" type="rootType" />
              <xs:complexType name="rootType">
                <xs:simpleContent>
                  <xs:extension base="xs:QName">
                    <xs:attribute name="name" type="xs:QName"/>
                  </xs:extension>
                </xs:simpleContent>
              </xs:complexType>
            </xs:schema>""")

        xml_data = '<root xmlns:ns0="http://xmlschema.test/0">ns0:foo</root>'
        self.assertEqual(schema.decode(xml_data),
                         {'@xmlns:ns0': 'http://xmlschema.test/0', '$': 'ns0:foo'})

        self.assertEqual(schema.decode('<root>foo</root>'), 'foo')

        with self.assertRaises(XMLSchemaValidationError) as ctx:
            schema.decode('<root>ns0:foo</root>')
        self.assertIn("failed validating 'ns0:foo'", str(ctx.exception))
        self.assertIn("Reason: unmapped prefix 'ns0' in a QName", str(ctx.exception))
        self.assertIn("Path: /root", str(ctx.exception))

        xml_data = '<root name="ns0:bar" xmlns:ns0="http://xmlschema.test/0">ns0:foo</root>'
        self.assertEqual(schema.decode(xml_data), {
            '@xmlns:ns0': 'http://xmlschema.test/0', '@name': 'ns0:bar', '$': 'ns0:foo'
        })

        # Check reverse encoding
        obj = schema.decode(xml_data, converter=JsonMLConverter)
        root = schema.encode(obj, converter=JsonMLConverter)
        self.assertEqual(ElementTree.tostring(root), b'<root name="ns0:bar">ns0:foo</root>\n')

        with self.assertRaises(XMLSchemaValidationError) as ctx:
            schema.decode('<root name="ns0:bar">foo</root>')
        self.assertIn("failed validating 'ns0:bar'", str(ctx.exception))
        self.assertIn("unmapped prefix 'ns0' in a QName", str(ctx.exception))
        self.assertIn("Path: /root", str(ctx.exception))

    def test_json_dump_and_load(self):
        vh_xml_tree = ElementTree.parse(self.vh_xml_file)
        col_xml_tree = ElementTree.parse(self.col_xml_file)
        with open(self.vh_json_file, 'w') as f:
            xmlschema.to_json(self.vh_xml_file, f)

        with open(self.vh_json_file) as f:
            root = xmlschema.from_json(f, self.vh_schema)

        os.remove(self.vh_json_file)
        self.check_etree_elements(vh_xml_tree.getroot(), root)

        with open(self.col_json_file, 'w') as f:
            xmlschema.to_json(self.col_xml_file, f)

        with open(self.col_json_file) as f:
            root = xmlschema.from_json(f, self.col_schema)

        os.remove(self.col_json_file)
        self.check_etree_elements(col_xml_tree.getroot(), root)

    def test_json_path_decoding(self):
        xml_file = self.col_xml_file
        schema = self.col_schema

        json_data = xmlschema.to_json(xml_file, schema=schema, path='*')
        self.assertIsInstance(json_data, str)
        self.assertEqual(len(json_data), 493)
        self.assertEqual(json_data[:4], '[{"@')
        self.assertEqual(json_data[-1], ']')

        self.assertEqual(
            json_data, xmlschema.to_json(xml_file, schema=schema, path='object')
        )
        self.assertEqual(
            json_data, xmlschema.to_json(xml_file, schema=schema, path='//object')
        )
        self.assertEqual(
            json_data, xmlschema.to_json(xml_file, schema=schema, path='/col:collection/object')
        )

    def test_json_lazy_decoding(self):
        kwargs = {'xml_document': self.col_xml_file, 'schema': self.col_schema}

        col_json = xmlschema.to_json(**kwargs)
        self.assertIsInstance(col_json, str)
        self.assertEqual(len(col_json), 688)
        self.assertTrue(col_json.startswith('{"@xmlns:'))
        self.assertEqual(col_json[-1], '}')

        self.assertEqual(
            col_json, xmlschema.to_json(lazy=True, **kwargs)
        )

        json_data = xmlschema.to_json(path='object', **kwargs)
        self.assertIn(json_data, col_json)
        self.assertEqual(
            json_data, xmlschema.to_json(path='object', lazy=True, **kwargs)
        )
        self.assertEqual(
            json_data, xmlschema.to_json(validation='skip', path='object', lazy=True, **kwargs)
        )

        json_data = xmlschema.to_json(path='object/author', **kwargs)
        self.assertIsInstance(json_data, str)
        self.assertEqual(len(json_data), 259)
        self.assertEqual(json_data[:4], '[{"@')
        self.assertEqual(json_data[-1], ']')

        self.assertEqual(
            json_data, xmlschema.to_json(path='object/author', lazy=True, **kwargs)
        )
        self.assertEqual(json_data, xmlschema.to_json(
            validation='skip', path='object/author', lazy=True, **kwargs
        ))

        # Tests for issue #159
        self.assertEqual(json_data, xmlschema.to_json(
            path='/col:collection/object/author', lazy=True, **kwargs
        ))
        self.assertEqual(json_data, xmlschema.to_json(
            validation='skip', path='/col:collection/object/author', lazy=True, **kwargs
        ))

    def test_path(self):
        xt = ElementTree.parse(self.vh_xml_file)
        xd = self.vh_schema.to_dict(xt, '/vh:vehicles/vh:cars', namespaces=self.vh_namespaces)
        self.assertEqual(xd['vh:car'], VEHICLES_DICT['vh:cars']['vh:car'])
        xd = self.vh_schema.to_dict(xt, '/vh:vehicles/vh:bikes', namespaces=self.vh_namespaces)
        self.assertEqual(xd['vh:bike'], VEHICLES_DICT['vh:bikes']['vh:bike'])

    def test_max_depth_argument(self):
        schema = self.schema_class(self.col_xsd_file)
        self.assertEqual(
            schema.decode(self.col_xml_file, max_depth=1),
            {'@xmlns:col': 'http://example.com/ns/collection',
             '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
             '@xsi:schemaLocation': 'http://example.com/ns/collection collection.xsd'})

        xmlschema.limits.MAX_XML_DEPTH = 1
        with self.assertRaises(XMLSchemaValidationError):
            schema.decode(self.col_xml_file)
        xmlschema.limits.MAX_XML_DEPTH = 9999

        self.assertEqual(
            schema.decode(self.col_xml_file, max_depth=2),
            {'@xmlns:col': 'http://example.com/ns/collection',
             '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
             '@xsi:schemaLocation': 'http://example.com/ns/collection collection.xsd',
             'object': [{'@id': 'b0836217462', '@available': True},
                        {'@id': 'b0836217463', '@available': True}]})

    def test_value_hook_argument(self):
        schema = self.schema_class(self.col_xsd_file)

        def ascii_strings(value, xsd_type):
            try:
                if not isinstance(value, str) or \
                        xsd_type.primitive_type.name != XSD_STRING:
                    return value
            except AttributeError:
                return value
            else:
                return value.encode('utf-8')

        obj = schema.decode(self.col_xml_file, value_hook=ascii_strings)
        self.assertNotEqual(obj, schema.decode(self.col_xml_file))
        root = schema.encode(obj)
        self.assertIsNone(etree_elements_assert_equal(
            root, ElementTree.parse(self.col_xml_file).getroot(), strict=False
        ))
        self.assertEqual(obj['object'][0]['title'], b'The Umbrellas')

    def test_non_global_schema_path(self):
        # Issue #157
        xs = self.schema_class("""<?xml version="1.0" encoding="UTF-8"?>
        <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
                xmlns:foo="http://example.com/foo"
                targetNamespace="http://example.com/foo">
            <xs:complexType name="type1">
                <xs:sequence>
                    <xs:element name="sub_part1" type="xs:string" />
                </xs:sequence>
            </xs:complexType>
            <xs:complexType name="type2">
                <xs:sequence>
                    <xs:element name="sub_part2" type="xs:string" />
                </xs:sequence>
            </xs:complexType>
            <xs:element name="foo">
                <xs:complexType>
                    <xs:sequence>
                        <xs:element name="part1" type="foo:type1" />
                        <xs:element name="part2" type="foo:type2" />
                    </xs:sequence>
                </xs:complexType>
            </xs:element>
        </xs:schema>""")

        self.assertEqual(
            xs.to_dict(
                """<part1 xmlns:foo="http://example.com/foo">
                    <sub_part1>test</sub_part1>
                </part1>""",
                schema_path='.//part1',
            ),
            {"sub_part1": "test"}
        )

    def test_validation_strict(self):
        self.assertRaises(
            xmlschema.XMLSchemaValidationError,
            self.vh_schema.to_dict,
            ElementTree.parse(self.casepath('examples/vehicles/vehicles-2_errors.xml')),
            validation='strict',
            namespaces=self.vh_namespaces
        )

    def test_validation_skip(self):
        xt = ElementTree.parse(self.casepath('features/decoder/data3.xml'))
        xd = self.st_schema.decode(xt, validation='skip', namespaces={'ns': 'ns'})
        self.assertEqual(xd['decimal_value'], ['abc'])

    def test_datatypes(self):
        xd = self.st_schema.to_dict(self.casepath('features/decoder/data.xml'))
        self.assertDictEqual(xd, DATA_DICT)

        xt = ElementTree.parse(self.casepath('features/decoder/data.xml'))
        xd = self.st_schema.to_dict(xt, namespaces=self.default_namespaces)
        self.assertNotEqual(xd, DATA_DICT)
        self.assertIn('@xmlns:xsi', xd)
        self.assertIn('@xmlns:tns', xd)
        self.assertIn('@xmlns:ns', xd)

    def test_datetime_types(self):
        xs = self.get_schema('<xs:element name="dt" type="xs:dateTime"/>')
        self.assertEqual(xs.decode('<dt>2019-01-01T13:40:00</dt>'), '2019-01-01T13:40:00')
        self.assertEqual(xs.decode('<dt>2019-01-01T13:40:00</dt>', datetime_types=True),
                         datatypes.DateTime10.fromstring('2019-01-01T13:40:00'))

        xs = self.get_schema('<xs:element name="dt" type="xs:date"/>')
        self.assertEqual(xs.decode('<dt>2001-04-15</dt>'), '2001-04-15')
        self.assertEqual(xs.decode('<dt>2001-04-15</dt>', datetime_types=True),
                         datatypes.Date10.fromstring('2001-04-15'))

        xs = self.get_schema('<xs:attribute name="dt" type="xs:date"/>')
        self.assertEqual(xs.attributes['dt'].decode('2001-04-15'), '2001-04-15')
        self.assertEqual(xs.attributes['dt'].decode('2001-04-15', datetime_types=True),
                         datatypes.Date10.fromstring('2001-04-15'))

    def test_duration_type(self):
        xs = self.get_schema('<xs:element name="td" type="xs:duration"/>')
        self.assertEqual(xs.decode('<td>P5Y3MT60H30.001S</td>'), 'P5Y3MT60H30.001S')
        self.assertEqual(xs.decode('<td>P5Y3MT60H30.001S</td>', datetime_types=True),
                         datatypes.Duration.fromstring('P5Y3M2DT12H30.001S'))

    def test_default_converter(self):
        self.assertEqual(self.col_schema.to_dict(self.col_xml_file), COLLECTION_DICT)

        default_dict = self.col_schema.to_dict(self.col_xml_file,
                                               converter=xmlschema.XMLSchemaConverter)
        self.assertEqual(default_dict, COLLECTION_DICT)

        default_dict_root = self.col_schema.to_dict(self.col_xml_file, preserve_root=True)
        self.assertEqual(default_dict_root, {'col:collection': COLLECTION_DICT})

    def test_visitor_converter(self):
        visitor_dict = self.col_schema.to_dict(self.col_xml_file, converter=UnorderedConverter)
        self.assertEqual(visitor_dict, COLLECTION_DICT)

        visitor_dict_root = self.col_schema.to_dict(
            self.col_xml_file, converter=UnorderedConverter(preserve_root=True))
        self.assertEqual(visitor_dict_root, {'col:collection': COLLECTION_DICT})

    def test_parker_converter(self):
        parker_dict = self.col_schema.to_dict(self.col_xml_file,
                                              converter=xmlschema.ParkerConverter)
        self.assertEqual(parker_dict, COLLECTION_PARKER)

        parker_dict_root = self.col_schema.to_dict(
            self.col_xml_file, converter=ParkerConverter(preserve_root=True), decimal_type=float)
        self.assertEqual(parker_dict_root, COLLECTION_PARKER_ROOT)

    def test_badgerfish_converter(self):
        badgerfish_dict = self.col_schema.to_dict(
            self.col_xml_file, converter=BadgerFishConverter, decimal_type=float)
        self.assertEqual(badgerfish_dict, COLLECTION_BADGERFISH)

    def test_abdera_converter(self):
        abdera_dict = self.col_schema.to_dict(
            self.col_xml_file, converter=AbderaConverter, decimal_type=float, dict_class=dict)
        self.assertEqual(abdera_dict, COLLECTION_ABDERA)

    def test_json_ml_converter(self):
        json_ml_dict = self.col_schema.to_dict(self.col_xml_file, converter=JsonMLConverter)
        self.assertEqual(json_ml_dict, COLLECTION_JSON_ML)

    def test_columnar_converter(self):
        columnar_dict = self.col_schema.to_dict(self.col_xml_file, converter=ColumnarConverter)
        self.assertEqual(columnar_dict, COLLECTION_COLUMNAR)
        columnar_dict = self.col_schema.to_dict(
            self.col_xml_file, converter=ColumnarConverter, attr_prefix='_',
        )
        self.assertEqual(columnar_dict, COLLECTION_COLUMNAR_)

        with self.assertRaises(ValueError) as ctx:
            self.col_schema.to_dict(
                self.col_xml_file, converter=ColumnarConverter, attr_prefix='-',
            )
        self.assertEqual(str(ctx.exception),
                         "'attr_prefix' can be the empty string or a single/double underscore")

    def test_dict_granularity(self):
        """Based on Issue #22, test to make sure an xsd indicating list with
        dictionaries, returns just that even when it has a single dict. """
        xsd_string = self.casepath('issues/issue_022/xsd_string.xsd')
        xml_string_1 = self.casepath('issues/issue_022/xml_string_1.xml')
        xml_string_2 = self.casepath('issues/issue_022/xml_string_2.xml')
        xsd_schema = self.schema_class(xsd_string)
        xml_data_1 = xsd_schema.to_dict(xml_string_1)
        xml_data_2 = xsd_schema.to_dict(xml_string_2)
        self.assertTrue(isinstance(xml_data_1['bar'], type(xml_data_2['bar'])),
                        msg="XSD with an array that return a single element from "
                            "xml must still yield a list.")

    def test_any_type_decoding(self):
        any_type = self.schema_class.meta_schema.types['anyType']
        xml_data_1 = ElementTree.Element('dummy')
        self.assertIsNone(any_type.decode(xml_data_1))
        xml_data_2 = ElementTree.fromstring('<root>\n    <child_1/>\n    <child_2/>\n</root>')

        # Fix for processContents='lax' (issue 273, previously result was None)
        self.assertEqual(any_type.decode(xml_data_2), {'child_1': [None], 'child_2': [None]})

    def test_skip_wildcard_decoding(self):
        schema = self.schema_class("""<?xml version="1.0" encoding="UTF-8"?>
            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
                <xs:element name="foo">
                    <xs:complexType>
                        <xs:sequence>
                            <xs:any processContents="skip"/>
                        </xs:sequence>
                    </xs:complexType>
                </xs:element>
            </xs:schema>""")

        self.assertIsNone(schema.decode('<foo><bar/></foo>'))

        obj = schema.decode('<foo><bar/></foo>', process_skipped=True)
        self.assertEqual(obj, {'bar': None})

        root = schema.encode(obj)
        self.assertEqual(root.tag, 'foo')
        self.assertEqual(len(root), 0)

        root = schema.encode(obj, process_skipped=True)
        self.assertEqual(root.tag, 'foo')
        self.assertEqual(len(root), 1)
        self.assertEqual(root[0].tag, 'bar')

    def test_any_type_decoding__issue_273(self):
        schema = self.schema_class(self.casepath('issues/issue_273/issue_273.xsd'))
        data = schema.to_dict(self.casepath('issues/issue_273/issue_273.xml'))

        self.assertDictEqual(data, {
            '@x': 'QA01_sequence',
            '@y': 2500,
            'QA01': [
                {'qa01_elem01': ['13'],
                 'qa01_elem02': ['5139'],
                 'qa01_elem03': ['170'],
                 'qa01_elem04': [{'@these': 'attributes', '@get': 'dropped', '$': '0'}],
                 'qa01_elem05': ['56'],
                 'qa01_elem06': ['11141178']
                 }]})

    def test_choice_model_decoding__issue_041(self):
        schema = self.schema_class(self.casepath('issues/issue_041/issue_041.xsd'))
        data = schema.to_dict(self.casepath('issues/issue_041/issue_041.xml'))
        self.assertEqual(data, {
            '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
            '@xsi:noNamespaceSchemaLocation': 'issue_041.xsd',
            'Name': 'SomeNameValueThingy',
            'Value': {'Integer': 0}
        })

    def test_cdata_decoding(self):
        schema = self.schema_class(self.casepath('issues/issue_046/issue_046.xsd'))
        xml_file = self.casepath('issues/issue_046/issue_046.xml')
        self.assertEqual(
            schema.decode(xml_file, cdata_prefix='#'),
            dict(
                [('@xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'),
                 ('@xsi:noNamespaceSchemaLocation', 'issue_046.xsd'),
                 ('#1', 'Dear Mr.'), ('name', 'John Smith'),
                 ('#2', '.\n  Your order'), ('orderid', 1032),
                 ('#3', 'will be shipped on'), ('shipdate', '2001-07-13'), ('#4', '.')]
            ))

    def test_string_facets(self):
        none_empty_string_type = self.st_schema.types['none_empty_string']
        self.check_decode(none_empty_string_type, '', XMLSchemaValidationError)
        name_type = self.st_schema.types['NameType']
        self.check_decode(name_type, '', XMLSchemaValidationError)

    def test_hex_binary_type(self):
        hex_code_type = self.st_schema.types['hexCode']
        self.check_decode(hex_code_type, '00D7310A', datatypes.HexBinary(b'00D7310A'))
        self.check_decode(hex_code_type, 'D7310A', XMLSchemaValidationError)

        xs = self.get_schema('<xs:element name="hex" type="xs:hexBinary"/>')

        obj = xs.decode('<hex> 9AFD </hex>')
        self.assertEqual(obj, '9AFD')  # Value string is collapsed anyway
        self.assertIsInstance(obj, str)

        obj = xs.decode('<hex> 9AFD </hex>', binary_types=True)
        self.assertIsInstance(obj, datatypes.HexBinary)
        self.assertEqual(obj.value, b'9AFD')

        xs = self.get_schema('<xs:attribute name="hex" type="xs:hexBinary"/>')

        obj = xs.attributes['hex'].decode(' 9AFD ')
        self.assertEqual(obj, '9AFD')
        self.assertIsInstance(obj, str)

        obj = xs.attributes['hex'].decode(' 9AFD ', binary_types=True)
        self.assertIsInstance(obj, datatypes.HexBinary)
        self.assertEqual(obj.value, b'9AFD')

    def test_base64_binary_type(self):
        base64_code_type = self.st_schema.types['base64Code']
        self.check_decode(base64_code_type, base64.b64encode(b'ok'), XMLSchemaValidationError)
        base64_value = base64.b64encode(b'hello')

        expected_value = datatypes.Base64Binary(base64_value)
        self.check_decode(base64_code_type, base64_value, expected_value)

        # Attribute
        xs = self.get_schema('<xs:attribute name="b64" type="xs:base64Binary"/>')

        obj = xs.attributes['b64'].decode(base64_value.decode())
        self.assertEqual(obj, str(expected_value))
        self.assertIsInstance(obj, str)

        obj = xs.attributes['b64'].decode(base64_value.decode(), binary_types=True)
        self.assertEqual(obj, expected_value)
        self.assertIsInstance(obj, datatypes.Base64Binary)

        # Element
        xs = self.get_schema('<xs:element name="b64" type="xs:base64Binary"/>')

        obj = xs.decode(f'<b64>{base64_value.decode()}</b64>')
        self.assertEqual(obj, str(expected_value))
        self.assertIsInstance(obj, str)

        obj = xs.decode(f'<b64>{base64_value.decode()}</b64>', binary_types=True)
        self.assertEqual(obj, expected_value)
        self.assertIsInstance(obj, datatypes.Base64Binary)

        self.check_decode(base64_code_type, base64.b64encode(b'abcefgh'),
                          datatypes.Base64Binary('YWJjZWZnaA=='))
        self.check_decode(base64_code_type, b' Y  W J j ZWZ\t\tn\na A= =',
                          datatypes.Base64Binary('Y W J j ZWZ n a A= ='))
        self.check_decode(base64_code_type, ' Y  W J j ZWZ\t\tn\na A= =',
                          datatypes.Base64Binary('Y W J j ZWZ n a A= ='))
        self.check_decode(base64_code_type, base64.b64encode(b'abcefghi'),
                          datatypes.Base64Binary('YWJjZWZnaGk='))

        self.check_decode(base64_code_type, 'YWJjZWZnaA=', XMLSchemaValidationError)
        self.check_decode(base64_code_type, 'YWJjZWZna$==', XMLSchemaValidationError)

        base64_length4_type = self.st_schema.types['base64Length4']
        self.check_decode(base64_length4_type, base64.b64encode(b'abc'),
                          XMLSchemaValidationError)
        self.check_decode(base64_length4_type, base64.b64encode(b'abce'),
                          datatypes.Base64Binary('YWJjZQ=='))
        self.check_decode(base64_length4_type, base64.b64encode(b'abcef'),
                          XMLSchemaValidationError)

        base64_length5_type = self.st_schema.types['base64Length5']
        self.check_decode(base64_length5_type, base64.b64encode(b'1234'),
                          XMLSchemaValidationError)
        self.check_decode(base64_length5_type, base64.b64encode(b'12345'),
                          datatypes.Base64Binary('MTIzNDU='))
        self.check_decode(base64_length5_type, base64.b64encode(b'123456'),
                          XMLSchemaValidationError)

    def test_decimal_type(self):
        schema = self.get_schema("""
        <xs:element name="A" type="A_type" />
        <xs:simpleType name="A_type">
          <xs:restriction base="xs:decimal">
            <xs:minInclusive value="100.50"/>
          </xs:restriction>
        </xs:simpleType>
        """)

        self.check_decode(schema, '<A>120.48</A>', Decimal('120.48'))
        self.check_decode(schema, '<A>100.50</A>', Decimal('100.50'), process_namespaces=False)
        self.check_decode(schema, '<A>100.49</A>', XMLSchemaValidationError)
        self.check_decode(schema, '<A>120.48</A>', 120.48, decimal_type=float)
        # Issue #66
        self.check_decode(schema, '<A>120.48</A>', '120.48', decimal_type=str)

    def test_nillable__issue_076(self):
        xsd_string = """<?xml version="1.0" encoding="UTF-8"?>
        <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
            <xs:element name="foo" type="Foo" />
            <xs:complexType name="Foo">
                <xs:sequence minOccurs="1" maxOccurs="1">
                    <xs:element name="bar" type="xs:integer" nillable="true" />
                </xs:sequence>
            </xs:complexType>
        </xs:schema>
        """
        xsd_schema = self.schema_class(xsd_string)
        xml_string_1 = "<foo><bar>0</bar></foo>"
        xml_string_2 = """<?xml version="1.0" encoding="UTF-8"?>
        <foo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <bar xsi:nil="true"></bar>
        </foo>
        """
        self.assertTrue(xsd_schema.is_valid(source=xml_string_1, use_defaults=False))
        self.assertTrue(xsd_schema.is_valid(source=xml_string_2, use_defaults=False))
        obj = xsd_schema.decode(xml_string_2, use_defaults=False)
        self.check_etree_elements(ElementTree.fromstring(xml_string_2), xsd_schema.encode(obj))

    def test_keep_empty(self):
        schema = self.schema_class(self.casepath('issues/issue_322/issue_322.xsd'))
        xml_file = self.casepath('issues/issue_322/issue_322.xml')

        data = schema.decode(xml_file)
        self.assertEqual(data, {
            '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
            'emptystring': None,
            'nillstring': {'@xsi:nil': 'true'}
        })

        data = schema.decode(xml_file, keep_empty=True)
        self.assertEqual(data, {
            '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
            'emptystring': '',
            'nillstring': {'@xsi:nil': 'true'}
        })

        schema = self.schema_class(dedent("""\
        <?xml version="1.0"?>
        <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
          <xs:element name="note">
            <xs:complexType>
              <xs:sequence>
                <xs:element name="emptiable" type="xs:string"/>
                <xs:element name="filled">
                  <xs:simpleType>
                    <xs:restriction base="xs:string">
                      <xs:minLength value="1"/>
                    </xs:restriction>
                  </xs:simpleType>
                </xs:element>
                <xs:element name="number" type="xs:int"/>
              </xs:sequence>
            </xs:complexType>
          </xs:element>
        </xs:schema>"""))

        xml_data = "<note><emptiable/><filled/><number/></note>"
        data, errors = schema.decode(xml_data, validation='lax', keep_empty=True)
        self.assertEqual(data, {'emptiable': '', 'filled': '', 'number': None})
        self.assertEqual(len(errors), 2)

        data = schema.decode(xml_data, validation='skip', keep_empty=True)
        self.assertEqual(data, {'emptiable': '', 'filled': '', 'number': ''})

    def test_element_hook__issue_322(self):
        schema = self.schema_class(self.casepath('issues/issue_322/issue_322.xsd'))
        xml_file = self.casepath('issues/issue_322/issue_322.xml')

        def element_hook(element_data: ElementData, *_args):
            if not element_data.attributes:
                return element_data

            return ElementData(
                element_data.tag,
                element_data.text,
                element_data.content,
                [x for x in element_data.attributes if x[0] != XSI_NIL],
                None
            )

        data = schema.decode(xml_file, element_hook=element_hook)
        self.assertEqual(data, {
            '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
            'emptystring': None,
            'nillstring': None,
        })

        # Resolution for issue 322
        data = schema.decode(xml_file, keep_empty=True, element_hook=element_hook)
        self.assertEqual(data, {
            '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
            'emptystring': '',
            'nillstring': None,
        })

    def test_default_namespace__issue_077(self):
        xs = self.schema_class("""<?xml version="1.0" encoding="UTF-8"?>
        <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
            targetNamespace="http://example.com/foo">
          <xs:element name="foo" type="xs:string" />
        </xs:schema>""")
        self.assertEqual(xs.to_dict("""<foo xmlns="http://example.com/foo">bar</foo>""",
                                    path='/foo', namespaces={'': 'http://example.com/foo'}),
                         {'@xmlns': 'http://example.com/foo', '$': 'bar'})

        self.assertEqual(xs.to_dict("""<foo>bar</foo>""",
                                    path='/foo', namespaces={'': 'http://example.com/foo'}), None)

    def test_complex_with_simple_content_restriction(self):
        xs = self.schema_class(
            self.casepath('features/derivations/complex-with-simple-content-restriction.xsd')
        )
        self.assertTrue(xs.is_valid('<value>10</value>'))
        self.assertFalse(xs.is_valid('<value>alpha</value>'))
        self.assertEqual(xs.decode('<value>10</value>'), 10)

    def test_union_types__issue_103(self):
        decimal_or_nan = self.st_schema.types['myType']
        self.check_decode(decimal_or_nan, '95.0', Decimal('95.0'))
        self.check_decode(decimal_or_nan, 'NaN', 'NaN')

    def test_default_values__issue_108(self):
        # From issue #108
        xsd_text = """<?xml version="1.0" encoding="utf-8"?>
            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
              <xs:element name="root" type="root" default="default_value"/>
              <xs:complexType name="root">
                <xs:simpleContent>
                  <xs:extension base="xs:string">
                    <xs:attribute name="attr" type="xs:string"/>
                    <xs:attribute name="attrWithDefault" type="xs:string" default="default_value"/>
                    <xs:attribute name="attrWithFixed" type="xs:string" fixed="fixed_value"/>
                  </xs:extension>
                </xs:simpleContent>
              </xs:complexType>
              <xs:element name="simple_root" type="xs:string" default="default_value"/>
            </xs:schema>"""

        schema = self.schema_class(xsd_text)
        self.assertEqual(schema.to_dict("<root>text</root>"),
                         {'@attrWithDefault': 'default_value',
                          '@attrWithFixed': 'fixed_value',
                          '$': 'text'})
        self.assertEqual(schema.to_dict("<root/>"),
                         {'@attrWithDefault': 'default_value',
                          '@attrWithFixed': 'fixed_value',
                          '$': 'default_value'})
        self.assertEqual(schema.to_dict("""<root attr="attr_value">text</root>"""),
                         {'$': 'text',
                          '@attr': 'attr_value',
                          '@attrWithDefault': 'default_value',
                          '@attrWithFixed': 'fixed_value'})

        self.assertEqual(schema.to_dict("<root>text</root>", use_defaults=False),
                         {'@attrWithFixed': 'fixed_value', '$': 'text'})
        self.assertEqual(schema.to_dict("""<root attr="attr_value">text</root>""",
                                        use_defaults=False),
                         {'$': 'text', '@attr': 'attr_value', '@attrWithFixed': 'fixed_value'})
        self.assertEqual(schema.to_dict("<root/>", use_defaults=False),
                         {'@attrWithFixed': 'fixed_value'})

        self.assertEqual(schema.to_dict("<simple_root/>"), 'default_value')
        self.assertIsNone(schema.to_dict("<simple_root/>", use_defaults=False))

    def test_decoding_errors_with_validation_modes(self):
        schema = self.schema_class("""<?xml version="1.0" encoding="utf-8"?>
            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
              <xs:element name="root" type="rootType" />
              <xs:complexType name="rootType">
                <xs:simpleContent>
                  <xs:extension base="xs:int">
                    <xs:attribute name="int_attr" type="xs:int"/>
                    <xs:attribute name="bool_attr" type="xs:boolean"/>
                  </xs:extension>
                </xs:simpleContent>
              </xs:complexType>
              <xs:element name="simple_root" type="xs:float"/>
            </xs:schema>""")

        self.assertIsNone(schema.to_dict("<simple_root>alpha</simple_root>", validation='lax')[0])
        self.assertEqual(schema.to_dict("<root int_attr='10'>20</root>"),
                         {'@int_attr': 10, '$': 20})
        self.assertEqual(schema.to_dict("<root int_attr='wrong'>20</root>", validation='lax')[0],
                         {'@int_attr': None, '$': 20})
        self.assertEqual(schema.to_dict("<root int_attr='wrong'>20</root>", validation='skip'),
                         {'@int_attr': 'wrong', '$': 20})

    def test_keep_unknown_tags__issue_204(self):
        schema = self.schema_class(self.casepath('issues/issue_204/issue_204.xsd'))
        self.assertTrue(schema.is_valid(self.casepath('issues/issue_204/issue_204_1.xml')))
        self.assertFalse(schema.is_valid(self.casepath('issues/issue_204/issue_204_2.xml')))

        data = schema.decode(self.casepath('issues/issue_204/issue_204_2.xml'), validation='lax')
        self.assertEqual({x for x in data[0] if x[0] != '@'}, {'child2', 'child5'})

        data = schema.decode(self.casepath('issues/issue_204/issue_204_3.xml'), validation='lax')
        self.assertEqual({x for x in data[0] if x[0] != '@'}, {'child2', 'child5'})

        data = schema.decode(self.casepath('issues/issue_204/issue_204_3.xml'),
                             validation='lax', keep_unknown=True)
        self.assertEqual({x for x in data[0] if x[0] != '@'}, {'child2', 'unknown', 'child5'})
        self.assertEqual(data[0]['unknown'], {'a': ['1'], 'b': [None]})

        data = schema.decode(self.casepath('issues/issue_204/issue_204_2.xml'), validation='skip')
        self.assertEqual({x for x in data if x[0] != '@'}, {'child2', 'child5'})

        data = schema.decode(self.casepath('issues/issue_204/issue_204_3.xml'),
                             validation='skip', keep_unknown=True)
        self.assertEqual({x for x in data if x[0] != '@'}, {'child2', 'unknown', 'child5'})
        self.assertEqual(data['unknown'], {'a': ['1'], 'b': [None]})

    def test_error_message__issue_115(self):
        schema = self.schema_class(self.casepath('issues/issue_115/Rotation.xsd'))
        rotation_data = '<tns:rotation xmlns:tns="http://www.example.org/Rotation/" ' \
                        'pitch="0.0" roll="0.0" yaw="-1.0" />'

        message_lines = []
        try:
            schema.decode(rotation_data)
        except Exception as err:
            message_lines = str(err).split('\n')

        self.assertTrue(message_lines, msg="Empty error message!")
        self.assertEqual(message_lines[-6], 'Instance:')
        self.assertEqual(message_lines[-4].strip(), rotation_data)
        self.assertEqual(message_lines[-2], 'Path: /tns:rotation')

    def test_empty_base_type_extension_single_value(self):
        xsd_text = """<?xml version="1.0" encoding="utf-8"?>
            <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            >
              <xs:element name="root" type="rootType" />

              <xs:complexType name="BaseType" abstract="true">
                <xs:sequence />
              </xs:complexType>


              <xs:complexType name="ExtensionType">
                <xs:complexContent>
                  <xs:extension base="BaseType">
                    <xs:sequence>
                      <xs:element name="Content" type="xs:string" />
                    </xs:sequence>
                  </xs:extension>
                </xs:complexContent>
              </xs:complexType>

              <xs:complexType name="rootType">
                <xs:sequence>
                    <xs:element name="ExtensionSub" type="BaseType" />
                </xs:sequence>
              </xs:complexType>
            </xs:schema>"""

        schema = self.schema_class(xsd_text)
        xml_source = """<root xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
              <ExtensionSub xsi:type="ExtensionType">
                <Content>my content</Content>
              </ExtensionSub>
            </root>"""

        result = schema.to_dict(xml_source)
        expected = {
            "@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
            "ExtensionSub": {
                "@xsi:type": "ExtensionType",
                "Content": "my content",
            }
        }
        self.assertEqual(result, expected)

        root = schema.to_etree(result)
        self.assertIsNone(etree_elements_assert_equal(
            root, ElementTree.XML(xml_source), strict=False
        ))

    def test_issue_190(self):
        # Changed is_single() for XsdElement to check also parent group.
        schema = self.schema_class(self.casepath('issues/issue_190/issue_190.xsd'))
        self.assertEqual(
            schema.to_dict(self.casepath('issues/issue_190/issue_190.xml')),
            {'a': {'c': ['1']}, 'b': {'c': ['1'], 'e': ['1']}}
        )

    def test_issue_200(self):
        # Schema path is required when path doesn't resolve to an XSD element
        schema = self.schema_class(self.casepath('issues/issue_200/issue_200.xsd'))
        self.assertEqual(
            schema.to_dict(self.casepath('issues/issue_200/issue_200.xml'),
                           path='/na:main/na:item[@doc_id=1]'),
            {'@doc_id': 1, '@ref_id': 'k1', '$': 'content_k1'}
        )
        with self.assertRaises(XMLSchemaValidationError) as ctx:
            schema.to_dict(self.casepath('issues/issue_200/issue_200.xml'),
                           path='/na:main/na:item[@doc_id=2]'),
        self.assertIn('is not an element of the schema', str(ctx.exception))

    def test_issue_238__decode_bytes_strings(self):
        with open(self.vh_xml_file, 'rb') as fp:
            vh_xml_data = fp.read()

        self.assertIsInstance(vh_xml_data, bytes)

        obj = self.vh_schema.decode(vh_xml_data)

        # ElementTree accepts also bytes but emits Unicode strings only
        for value in iter_nested_iterables(obj):
            self.assertNotIsInstance(value, bytes)

    def test_issue_240__decode_unicode_to_json(self):
        schema = self.get_schema('<xs:element name="chars" type="xs:string"/>')

        xml_data = '<chars>øæå</chars>'
        obj = xmlschema.to_dict(xml_data, schema=schema, decimal_type=str)
        self.assertEqual(obj, 'øæå')
        self.assertEqual(obj, schema.decode(xml_data, decimal_type=str))

        json_data = json.dumps(obj, indent=4)
        self.assertEqual(obj, 'øæå')
        self.assertIsInstance(obj, str)
        self.assertEqual(json_data.encode("utf-8"), b'"\\u00f8\\u00e6\\u00e5"')

    def test_float_decoding(self):
        for float_type in [self.schema_class.meta_schema.types['float'],
                           self.schema_class.meta_schema.types['double']]:
            self.assertAlmostEqual(float_type.decode('-1'), -1.0)
            self.assertAlmostEqual(float_type.decode('19.7'), 19.7)
            self.assertAlmostEqual(float_type.decode('INF'), float('inf'))
            self.assertAlmostEqual(float_type.decode('-INF'), float('-inf'))
            self.assertNotAlmostEqual(float_type.decode('INF'), float('-inf'))
            self.assertTrue(math.isnan(float_type.decode('NaN')))

    def test_no_process_namespaces__issue_314(self):
        xsd_file = self.casepath('issues/issue_314/issue_314.xsd')
        xml_file = self.casepath('issues/issue_314/issue_314.xml')
        schema = self.schema_class(xsd_file)

        result = schema.decode(xml_file)
        self.assertEqual(
            result,
            {'@xmlns:p': 'my_namespace',
             '@xmlns:b': 'http://www.w3.org/2001/XMLSchema-instance',
             'p:container': {
                 'p:item': [{
                     '@b:type': 'p:ConcreteContainterItemInfo',
                     '@attr_2': 'value_2'}
                 ]}}
        )

        result = schema.decode(xml_file, process_namespaces=False)
        self.assertEqual(
            result,
            {'{my_namespace}container': {
                '{my_namespace}item': [{
                    '@{http://www.w3.org/2001/XMLSchema-instance}type':
                        'p:ConcreteContainterItemInfo',
                    '@attr_2': 'value_2'}]}}
        )

    def test_mixed_content_decode__issue_334(self):
        schema = self.schema_class("""
        <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">

            <xs:complexType name="mixedContentType" mixed="true">
                <xs:sequence>
                    <xs:element name="elem1" maxOccurs="unbounded"/>
                </xs:sequence>
            </xs:complexType>

            <xs:element name="foo">
              <xs:complexType>
                <xs:complexContent>
                  <xs:extension base="mixedContentType">
                    <xs:attribute name="bar" type="xs:string" />
                  </xs:extension>
                </xs:complexContent>
              </xs:complexType>
            </xs:element>

            <xs:element name="root" type="mixedContentType" />

        </xs:schema>
        """)

        result = schema.decode('<root>text1<elem1/>tail1<elem1/>tail2</root>')
        self.assertEqual(result, {'elem1': [None, None]})

        result = schema.decode('<root>text1<elem1/>tail1<elem1/>tail2</root>', cdata_prefix='#')
        self.assertEqual(result, {'#1': 'text1',
                                  'elem1': [None, None],
                                  '#2': 'tail1',
                                  '#3': 'tail2'})
        result = schema.decode('<root>text1<elem1/>tail1<elem1/>tail2</root>', cdata_prefix='')
        self.assertEqual(result, {'1': 'text1',
                                  'elem1': [None, None],
                                  '2': 'tail1',
                                  '3': 'tail2'})

        xsd_file = self.casepath('issues/issue_334/issue_334.xsd')
        xml_file = self.casepath('issues/issue_334/issue_334.xml')
        xs = self.schema_class(xsd_file)
        result = xs.decode(xml_file)
        body_text = result['Demonstrative_Example'][0]['Body_Text']

        expected = ['The snippet of code below establishes a new cookie to hold the sessionID.',
                    'The HttpOnly flag is not set for the cookie. An attacker who can perform '
                    'XSS could insert malicious script such as:',
                    'When the client loads and executes this script, it makes a request to the '
                    'attacker-controlled web site. The attacker can then log the request and '
                    'steal the cookie.',
                    'To mitigate the risk, use the setHttpOnly(true) method.']
        self.assertListEqual(body_text, expected)

        result = xs.decode(xml_file, preserve_root=True)
        body_text = result['Demonstrative_Examples']['Demonstrative_Example'][0]['Body_Text']
        self.assertListEqual(body_text, expected)

    def test_fill_missing_elements__issue_341(self):
        xsd_file = self.casepath('issues/issue_341/issue_341.xsd')
        xml_file = self.casepath('issues/issue_341/issue_341.xml')
        schema = self.schema_class(xsd_file)

        expected = {'TEST_EL': [
            {'@Date': '2022-10-03',
             'TEST_EL_2': {
                 'exists_in_xml': {
                     '@test_attr': 'test_value_attr', '@test_attr_2': None
                 }
             }}
        ]}
        xml_dict = schema.decode(xml_file, fill_missing=True)
        self.assertDictEqual(xml_dict, expected)

        def fill_missing_content(element_data: ElementData, _xsd_element, xsd_type):
            group = xsd_type.model_group
            if group is None:
                return element_data  # an element with simple content

            filled_content = []
            model = ModelVisitor(group)
            xsd_element = None
            for name, value, xsd_element in element_data.content:
                if isinstance(name, int) or xsd_element is None:
                    filled_content.append((name, value, xsd_element))
                    continue

                while model.element is not None:
                    if model.element is xsd_element:
                        filled_content.append((name, value, xsd_element))
                        for _err in model.advance(True):
                            pass
                        break

                    if model.element.max_occurs != 0:
                        filled_content.append((model.element.name, None, model.element))
                    for _err in model.advance(False):
                        pass
                else:
                    filled_content.append((name, value, xsd_element))

            while model.element is not None:
                if model.element is not xsd_element and model.element.max_occurs != 0:
                    filled_content.append((model.element.name, None, model.element))
                for _err in model.advance(False):
                    pass

            return ElementData(
                element_data.tag,
                element_data.text,
                filled_content,
                element_data.attributes,
                None
            )

        expected = {'TEST_EL': [
            {'@Date': '2022-10-03',
             'TEST_EL_2': {
                 'exists_in_xml': {
                     '@test_attr': 'test_value_attr', '@test_attr_2': None
                 },
                 'not_exists_in_xml': None
             }}
        ]}
        xml_dict = schema.decode(xml_file, element_hook=fill_missing_content, fill_missing=True)
        self.assertDictEqual(xml_dict, expected)

        # Resolving more complex schemas requires more checks in hook function
        xsd_file = self.casepath('issues/issue_341/issue_341-ext.xsd')
        schema = self.schema_class(xsd_file)

        expected = {'TEST_EL': [
            {'@Date': '2022-10-03',
             'TEST_EL_2': {
                 'exists_in_xml': {
                     '@test_attr': 'test_value_attr', '@test_attr_2': None
                 },
                 'not_exists_in_xml': None,
                 'choice_elem1': None,
                 'choice_elem2': None,  # this is wrong (at most one element for a choice)
             }}
        ]}
        xml_dict = schema.decode(xml_file, element_hook=fill_missing_content, fill_missing=True)
        self.assertDictEqual(xml_dict, expected)

    def test_decoding_non_unicode_files(self):
        # Using cp1252 encoded XSD
        schema_file = self.casepath('examples/menù/menù-cp1252.xsd')
        schema = self.schema_class(schema_file)

        xml_file = self.casepath('examples/menù/menù-cp1252.xml')
        self.assertDictEqual(schema.decode(xml_file), MENU_DICT)

        xml_file = self.casepath('examples/menù/menù-ascii.xml')
        with self.assertRaises(ElementTree.ParseError):
            schema.decode(xml_file)  # Invalid XML file (entity in a tag name)

        xml_file = self.casepath('examples/menù/menù.xml')
        self.assertDictEqual(schema.decode(xml_file), MENU_DICT)

        # Using ASCII encoded XSD
        schema_file = self.casepath('examples/menù/menù-ascii.xsd')
        schema = self.schema_class(schema_file)

        xml_file = self.casepath('examples/menù/menù-cp1252.xml')
        self.assertDictEqual(schema.decode(xml_file), MENU_DICT)

        xml_file = self.casepath('examples/menù/menù.xml')
        self.assertDictEqual(schema.decode(xml_file), MENU_DICT)

    def test_xml_to_json_serialization(self):
        xml_file = self.casepath('serialization/document.xml')

        obj = xmlschema.to_json(
            xml_file,
            validation='skip',
            converter=xmlschema.ParkerConverter,
            xmlns_processing='stacked',
        )
        with open(self.casepath('serialization/parker.json')) as fp:
            json_data = json.load(fp)

        self.assertDictEqual(json.loads(obj), json_data)

        obj = xmlschema.to_json(
            xml_file,
            validation='skip',
            converter=xmlschema.BadgerFishConverter,
            xmlns_processing='stacked',
        )
        with open(self.casepath('serialization/badgerfish.json')) as fp:
            json_data = json.load(fp)

        self.assertDictEqual(json.loads(obj), json_data)

        obj = xmlschema.to_json(
            xml_file,
            validation='skip',
            converter=xmlschema.JsonMLConverter,
            xmlns_processing='stacked',
        )
        with open(self.casepath('serialization/jsonml.json')) as fp:
            json_data = json.load(fp)

        self.assertListEqual(json.loads(obj), json_data)

        obj = xmlschema.to_json(
            xml_file,
            validation='skip',
            converter=xmlschema.AbderaConverter,
            xmlns_processing='stacked',
        )
        with open(self.casepath('serialization/abdera.json')) as fp:
            json_data = json.load(fp)

        self.assertDictEqual(json.loads(obj), json_data)

    def test_xmlns_processing_argument(self):
        xsd_file = self.casepath('examples/collection/collection5.xsd')
        xml_file = self.casepath('examples/collection/collection-redef-xmlns.xml')

        schema = self.schema_class(xsd_file)
        self.assertTrue(schema.is_valid(xml_file))

        obj = schema.decode(xml_file)
        self.assertDictEqual(obj, COLLECTION_XMLNS_PROCESSING_STACKED)

        obj = schema.decode(xml_file, xmlns_processing='stacked')
        self.assertDictEqual(obj, COLLECTION_XMLNS_PROCESSING_STACKED)

        obj = schema.decode(xml_file, xmlns_processing='collapsed')
        self.assertDictEqual(obj, COLLECTION_XMLNS_PROCESSING_COLLAPSED)

        obj = schema.decode(xml_file, xmlns_processing='root-only')
        self.assertDictEqual(obj, COLLECTION_XMLNS_PROCESSING_ROOT_ONLY)

        namespaces = {'xsi-alt': 'http://www.w3.org/2001/XMLSchema-instance',
                      'col-alt': 'http://example.com/ns/collection'}
        obj = schema.decode(xml_file, xmlns_processing='none', namespaces=namespaces)
        self.assertDictEqual(obj, COLLECTION_XMLNS_PROCESSING_NONE)

        with self.assertRaises(ValueError):
            schema.decode(xml_file, xmlns_processing='precise')

        with self.assertRaises(TypeError):
            schema.decode(xml_file, xmlns_processing=True)

    def test_namespaces_argument(self):
        namespaces = {'vh': 'http://example.com/vehicles'}
        obj = self.vh_schema.decode(self.vh_xml_file, namespaces=namespaces)
        self.assertDictEqual(obj, VEHICLES_DICT)

        namespaces = {'vh2': 'http://example.com/vehicles'}
        obj = self.vh_schema.decode(self.vh_xml_file, namespaces=namespaces)
        self.assertDictEqual(obj, VEHICLES_DICT_OVERRIDE_PREFIX_1)

        namespaces = {'vh-alt': 'http://example.com/vehicles',
                      '': 'http://example.com/vehicles'}
        obj = self.vh_schema.decode(self.vh_xml_file, namespaces=namespaces)
        self.assertDictEqual(obj, VEHICLES_DICT_OVERRIDE_PREFIX_2)

        namespaces = {'': 'http://example.com/vehicles'}
        obj = self.vh_schema.decode(self.vh_xml_file, namespaces=namespaces)
        self.assertDictEqual(obj, VEHICLES_DICT_OVERRIDE_PREFIX_3)

    def test_validation_hook_argument(self):
        tags_num = 1

        def stop_decoding(_e, _xsd_element):
            nonlocal tags_num
            if tags_num >= 10:
                return True
            tags_num += 1
            return False

        obj = self.col_schema.decode(self.col_xml_file, validation_hook=stop_decoding)
        self.assertIn('object', obj)
        self.assertEqual(len(obj['object']), 1)


class TestDecoding11(TestDecoding):
    schema_class = XMLSchema11

    def test_datetime_types(self):
        xs = self.get_schema('<xs:element name="dt" type="xs:dateTime"/>')
        self.assertEqual(xs.decode('<dt>2019-01-01T13:40:00</dt>'), '2019-01-01T13:40:00')
        self.assertEqual(xs.decode('<dt>2019-01-01T13:40:00</dt>', datetime_types=True),
                         datatypes.DateTime.fromstring('2019-01-01T13:40:00'))

        xs = self.get_schema('<xs:element name="dt" type="xs:date"/>')
        self.assertEqual(xs.decode('<dt>2001-04-15</dt>'), '2001-04-15')
        self.assertEqual(xs.decode('<dt>2001-04-15</dt>', datetime_types=True),
                         datatypes.Date.fromstring('2001-04-15'))

        xs = self.get_schema('<xs:attribute name="dt" type="xs:date"/>')
        self.assertEqual(xs.attributes['dt'].decode('2001-04-15'), '2001-04-15')
        self.assertEqual(xs.attributes['dt'].decode('2001-04-15', datetime_types=True),
                         datatypes.Date.fromstring('2001-04-15'))

    def test_derived_duration_types(self):
        xs = self.get_schema('<xs:element name="td" type="xs:yearMonthDuration"/>')
        self.assertEqual(xs.decode('<td>P0Y4M</td>'), 'P0Y4M')
        self.assertEqual(xs.decode('<td>P2Y10M</td>', datetime_types=True),
                         datatypes.Duration.fromstring('P2Y10M'))

        xs = self.get_schema('<xs:element name="td" type="xs:dayTimeDuration"/>')
        self.assertEqual(xs.decode('<td>P2DT6H30M30.001S</td>'), 'P2DT6H30M30.001S')
        self.assertEqual(xs.decode('<td>P2DT26H</td>'), 'P2DT26H')
        self.assertEqual(xs.decode('<td>P2DT6H30M30.001S</td>', datetime_types=True),
                         datatypes.Duration.fromstring('P2DT6H30M30.001S'))

    def test_type_alternatives(self):
        xs = self.schema_class(self.casepath('features/elements/type_alternatives-no-ns.xsd'))
        self.assertTrue(xs.is_valid('<value choice="int">10</value>'))
        self.assertFalse(xs.is_valid('<value choice="int">10.1</value>'))
        self.assertTrue(xs.is_valid('<value choice="float">10.1</value>'))
        self.assertFalse(xs.is_valid('<value choice="float">alpha</value>'))
        self.assertFalse(xs.is_valid('<value choice="bool">alpha</value>'))
        self.assertTrue(xs.is_valid('<value choice="bool">0</value>'))
        self.assertTrue(xs.is_valid('<value choice="bool">true</value>'))

        xs = self.schema_class(self.casepath('features/elements/type_alternatives.xsd'))
        self.assertTrue(xs.is_valid('<ns:value xmlns:ns="ns" choice="int">10</ns:value>'))
        self.assertFalse(xs.is_valid('<ns:value xmlns:ns="ns" choice="int">10.1</ns:value>'))
        self.assertTrue(xs.is_valid('<ns:value xmlns:ns="ns" choice="float">10.1</ns:value>'))
        self.assertFalse(xs.is_valid('<ns:value xmlns:ns="ns" choice="float">alpha</ns:value>'))
        self.assertFalse(xs.is_valid('<ns:value xmlns:ns="ns" choice="bool">alpha</ns:value>'))
        self.assertTrue(xs.is_valid('<ns:value xmlns:ns="ns" choice="bool">0</ns:value>'))
        self.assertTrue(xs.is_valid('<ns:value xmlns:ns="ns" choice="bool">true</ns:value>'))


if __name__ == '__main__':
    import platform
    header_template = "Test xmlschema decoding with Python {} on {}"
    header = header_template.format(platform.python_version(), platform.platform())
    print('{0}\n{1}\n{0}'.format("*" * len(header), header))

    unittest.main()
