File: documents.py

package info (click to toggle)
python-xmlschema 4.1.0-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 5,208 kB
  • sloc: python: 39,174; xml: 1,282; makefile: 36
file content (702 lines) | stat: -rw-r--r-- 32,008 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
#
# 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 json
from io import IOBase, TextIOBase
from collections.abc import Iterator
from typing import Any, Optional, Type, Union, IO, BinaryIO, TextIO
from xml.etree import ElementTree

from xmlschema.exceptions import XMLSchemaTypeError, XMLSchemaValueError, XMLResourceError
from xmlschema.names import XSD_NAMESPACE, XSI_TYPE, XSD_SCHEMA
from xmlschema.aliases import ElementType, NsmapType, LocationsType, SchemaSourceType, \
    DecodeType, EncodeType, JsonDecodeType, XMLSourceType
from xmlschema.translation import gettext as _
from xmlschema.utils.etree import is_etree_document, etree_tostring
from xmlschema.utils.qnames import get_extended_qname, update_namespaces, get_namespace_map
from xmlschema.resources import fetch_schema_locations, XMLResource
from xmlschema.converters import ConverterType
from xmlschema.validators import check_validation_mode, XMLSchema10, XMLSchemaBase, \
    XMLSchemaValidationError

__all__ = ('from_json', 'is_valid', 'iter_errors', 'iter_decode', 'to_dict',
           'to_etree', 'to_json', 'validate', 'XmlDocument')

# Allowed keyword arguments for building schema and resource instances, if necessary.
COMMON_KWARGS = frozenset(
    ('base_url', 'allow', 'defuse', 'timeout', 'uri_mapper', 'opener', 'iterparse')
)
RESOURCE_KWARGS = COMMON_KWARGS.union(('lazy', 'thin_lazy', 'selector'))
SCHEMA_KWARGS = COMMON_KWARGS.union(
    ('loader_class', 'use_fallback', 'use_xpath3', 'use_meta', 'loglevel')
)


def get_context(xml_document: Union[XMLSourceType, XMLResource],
                schema: Optional[Union[XMLSchemaBase, SchemaSourceType]] = None,
                cls: Optional[Type[XMLSchemaBase]] = None,
                locations: Optional[LocationsType] = None,
                use_location_hints: bool = True,
                dummy_schema: bool = False,
                **kwargs: Any) -> tuple[XMLResource, XMLSchemaBase]:
    """
    Get the XML document validation/decode context.

    :return: an XMLResource instance and a schema instance.
    """
    resource: XMLResource

    if cls is None:
        cls = XMLSchema10
    elif not issubclass(cls, XMLSchemaBase):
        raise XMLSchemaTypeError(_("invalid schema class {!r}").format(cls))

    if isinstance(xml_document, XMLResource):
        resource = xml_document
    else:
        resource_kwargs = {k: v for k, v in kwargs.items() if k in RESOURCE_KWARGS}
        resource = XMLResource(xml_document, **resource_kwargs)

    if isinstance(schema, XMLSchemaBase) and resource.namespace in schema.maps.namespaces:
        return resource, schema
    if isinstance(resource, XmlDocument) and hasattr(resource, 'schema'):
        return resource, resource.schema

    if use_location_hints:
        try:
            schema_location, locations = fetch_schema_locations(resource, locations, **kwargs)
        except ValueError:
            pass
        else:
            schema_kwargs = {k: v for k, v in kwargs.items() if k in SCHEMA_KWARGS}
            schema_kwargs['locations'] = locations

            if schema is None or isinstance(schema, XMLSchemaBase):
                return resource, cls(schema_location, **schema_kwargs)
            else:
                return resource, cls(schema, **schema_kwargs)

    if isinstance(schema, XMLSchemaBase):
        return resource, schema  # fallback to a schema for a different namespace
    elif schema is not None:
        schema_kwargs = {k: v for k, v in kwargs.items() if k in SCHEMA_KWARGS}
        return resource, cls(schema, locations=locations, **schema_kwargs)
    elif XSD_NAMESPACE == resource.namespace:
        assert cls.meta_schema is not None
        return resource, cls.meta_schema
    elif dummy_schema or XSI_TYPE in resource.root.attrib:
        return resource, get_dummy_schema(resource.root.tag, cls)
    else:
        msg = _("cannot get a schema for XML data, provide a schema argument")
        raise XMLSchemaValueError(msg)


def get_dummy_schema(tag: str, cls: Type[XMLSchemaBase]) -> XMLSchemaBase:
    if tag.startswith('{'):
        namespace, name = tag[1:].split('}')
    else:
        namespace, name = '', tag

    if namespace:
        return cls(
            '<xs:schema xmlns:xs="{}" targetNamespace="{}">\n'
            '    <xs:element name="{}"/>\n'
            '</xs:schema>'.format(XSD_NAMESPACE, namespace, name)
        )
    else:
        return cls(
            '<xs:schema xmlns:xs="{}">\n'
            '    <xs:element name="{}"/>\n'
            '</xs:schema>'.format(XSD_NAMESPACE, name)
        )


def get_lazy_json_encoder(errors: list[XMLSchemaValidationError]) -> Type[json.JSONEncoder]:

    class JSONLazyEncoder(json.JSONEncoder):
        def default(self, obj: Any) -> Any:
            if isinstance(obj, Iterator):
                for result in obj:
                    if isinstance(result, XMLSchemaValidationError):
                        errors.append(result)
                    else:
                        return result
                return None
            return json.JSONEncoder.default(self, obj)

    return JSONLazyEncoder


def validate(xml_document: Union[XMLSourceType, XMLResource],
             schema: Optional[XMLSchemaBase] = None,
             cls: Optional[Type[XMLSchemaBase]] = None,
             path: Optional[str] = None,
             schema_path: Optional[str] = None,
             use_defaults: bool = True,
             namespaces: Optional[NsmapType] = None,
             locations: Optional[LocationsType] = None,
             use_location_hints: bool = True,
             **kwargs: Any) -> None:
    """
    Validates an XML document against a schema instance. This function builds an
    :class:`XMLSchema` object for validating the XML document. Raises an
    :exc:`XMLSchemaValidationError` if the XML document is not validated against
    the schema.

    :param xml_document: can be an :class:`XMLResource` instance, a file-like object a path \
    to a file or a URI of a resource or an Element instance or an ElementTree instance or \
    a string containing the XML data. If the passed argument is not an :class:`XMLResource` \
    instance a new one is built using this and *defuse*, *timeout* and *lazy* arguments.
    :param schema: can be a schema instance or a file-like object or a file path or a URL \
    of a resource or a string containing the schema.
    :param cls: class to use for building the schema instance (for default \
    :class:`XMLSchema10` is used).
    :param path: is an optional XPath expression that matches the elements of the XML \
    data that have to be decoded. If not provided the XML root element is used.
    :param schema_path: an XPath expression to select the XSD element to use for decoding. \
    If not provided the *path* argument or the *source* root tag are used.
    :param use_defaults: defines when to use element and attribute defaults for filling \
    missing required values.
    :param namespaces: is an optional mapping from namespace prefix to URI.
    :param locations: additional schema location hints, used if a schema instance \
    has to be built.
    :param use_location_hints: for default, in case a schema instance has \
    to be built, uses also schema locations hints provided within XML data. \
    set this option to `False` to ignore these schema location hints.
    :param kwargs: other optional arguments for building :class:`XMLResource` or \
    :class:`XMLSchema` instances provided as keyword arguments.
    """
    source, schema = get_context(
        xml_document, schema, cls, locations, use_location_hints, **kwargs
    )
    schema.validate(source, path, schema_path, use_defaults, namespaces,
                    use_location_hints=use_location_hints)


def is_valid(xml_document: Union[XMLSourceType, XMLResource],
             schema: Optional[XMLSchemaBase] = None,
             cls: Optional[Type[XMLSchemaBase]] = None,
             path: Optional[str] = None,
             schema_path: Optional[str] = None,
             use_defaults: bool = True,
             namespaces: Optional[NsmapType] = None,
             locations: Optional[LocationsType] = None,
             use_location_hints: bool = True,
             **kwargs: Any) -> bool:
    """
    Like :meth:`validate` except that do not raise an exception but returns ``True`` if
    the XML document is valid, ``False`` if it's invalid.
    """
    source, schema = get_context(
        xml_document, schema, cls, locations, use_location_hints, **kwargs
    )
    return schema.is_valid(source, path, schema_path, use_defaults, namespaces,
                           use_location_hints=use_location_hints)


def iter_errors(xml_document: Union[XMLSourceType, XMLResource],
                schema: Optional[XMLSchemaBase] = None,
                cls: Optional[Type[XMLSchemaBase]] = None,
                path: Optional[str] = None,
                schema_path: Optional[str] = None,
                use_defaults: bool = True,
                namespaces: Optional[NsmapType] = None,
                locations: Optional[LocationsType] = None,
                use_location_hints: bool = True,
                **kwargs: Any) -> Iterator[XMLSchemaValidationError]:
    """
    Creates an iterator for the errors generated by the validation of an XML document.
    Takes the same arguments of the function :meth:`validate`.
    """
    source, schema = get_context(
        xml_document, schema, cls, locations, use_location_hints, **kwargs
    )
    return schema.iter_errors(source, path, schema_path, use_defaults, namespaces,
                              use_location_hints=use_location_hints)


def iter_decode(xml_document: Union[XMLSourceType, XMLResource],
                schema: Optional[XMLSchemaBase] = None,
                cls: Optional[Type[XMLSchemaBase]] = None,
                path: Optional[str] = None,
                validation: str = 'lax',
                locations: Optional[LocationsType] = None,
                use_location_hints: bool = True,
                **kwargs: Any) -> Iterator[Union[Any, XMLSchemaValidationError]]:
    """
    Creates an iterator for decoding an XML source to a data structure. For default
    the document is validated during the decoding phase and if it's invalid then one
    or more :exc:`XMLSchemaValidationError` instances are yielded before the decoded data.

    :param xml_document: can be an :class:`XMLResource` instance, a file-like object a path \
    to a file or a URI of a resource or an Element instance or an ElementTree instance or \
    a string containing the XML data. If the passed argument is not an :class:`XMLResource` \
    instance a new one is built using this and *defuse*, *timeout* and *lazy* arguments.
    :param schema: can be a schema instance or a file-like object or a file path or a URL \
    of a resource or a string containing the schema.
    :param cls: class to use for building the schema instance (for default uses \
    :class:`XMLSchema10`).
    :param path: is an optional XPath expression that matches the elements of the XML \
    data that have to be decoded. If not provided the XML root element is used.
    :param validation: defines the XSD validation mode to use for decode, can be \
    'strict', 'lax' or 'skip'.
    :param locations: additional schema location hints, in case a schema instance \
    has to be built.
    :param use_location_hints: for default, in case a schema instance has \
    to be built, uses also schema locations hints provided within XML data. \
    set this option to `False` to ignore these schema location hints.
    :param kwargs: other optional arguments of :meth:`XMLSchemaBase.iter_decode` \
    or for building :class:`XMLResource` or :class:`XMLSchema` instances provided \
    as keyword arguments.
    :raises: :exc:`XMLSchemaValidationError` if the XML document is invalid and \
    ``validation='strict'`` is provided.
    """
    source, _schema = get_context(
        xml_document, schema, cls, locations, use_location_hints, **kwargs
    )
    yield from _schema.iter_decode(source, path=path, validation=validation,
                                   use_location_hints=use_location_hints, **kwargs)


def to_dict(xml_document: Union[XMLSourceType, XMLResource],
            schema: Optional[XMLSchemaBase] = None,
            cls: Optional[Type[XMLSchemaBase]] = None,
            path: Optional[str] = None,
            validation: str = 'strict',
            locations: Optional[LocationsType] = None,
            use_location_hints: bool = True,
            **kwargs: Any) -> DecodeType[Any]:
    """
    Decodes an XML document to a Python's nested dictionary. Takes the same arguments
    of the function :meth:`iter_decode`, but *validation* mode defaults to 'strict'.

    :return: an object containing the decoded data. If ``validation='lax'`` is provided \
    validation errors are collected and returned in a tuple with the decoded data.
    :raises: :exc:`XMLSchemaValidationError` if the XML document is invalid and \
    ``validation='strict'`` is provided.
    """
    source, _schema = get_context(
        xml_document, schema, cls, locations, use_location_hints, **kwargs
    )
    return _schema.decode(source, path=path, validation=validation,
                          use_location_hints=use_location_hints, **kwargs)


def to_json(xml_document: Union[XMLSourceType, XMLResource],
            fp: Optional[IO[str]] = None,
            schema: Optional[XMLSchemaBase] = None,
            cls: Optional[Type[XMLSchemaBase]] = None,
            path: Optional[str] = None,
            validation: str = 'strict',
            locations: Optional[LocationsType] = None,
            use_location_hints: bool = True,
            json_options: Optional[dict[str, Any]] = None,
            **kwargs: Any) -> JsonDecodeType:
    """
    Serialize an XML document to JSON. For default the XML data is validated during
    the decoding phase. Raises an :exc:`XMLSchemaValidationError` if the XML document
    is not validated against the schema.

    :param xml_document: can be an :class:`XMLResource` instance, a file-like object a path \
    to a file or a URI of a resource or an Element instance or an ElementTree instance or \
    a string containing the XML data. If the passed argument is not an :class:`XMLResource` \
    instance a new one is built using this and *defuse*, *timeout* and *lazy* arguments.
    :param fp: can be a :meth:`write()` supporting file-like object.
    :param schema: can be a schema instance or a file-like object or a file path or a URL \
    of a resource or a string containing the schema.
    :param cls: schema class to use for building the instance (for default uses \
    :class:`XMLSchema10`).
    :param path: is an optional XPath expression that matches the elements of the XML \
    data that have to be decoded. If not provided the XML root element is used.
    :param validation: defines the XSD validation mode to use for decode, can be \
    'strict', 'lax' or 'skip'.
    :param locations: additional schema location hints, in case the schema instance \
    has to be built.
    :param use_location_hints: for default, in case a schema instance has \
    to be built, uses also schema locations hints provided within XML data. \
    set this option to `False` to ignore these schema location hints.
    :param json_options: a dictionary with options for the JSON serializer.
    :param kwargs: optional arguments of :meth:`XMLSchemaBase.iter_decode` as keyword arguments \
    to variate the decoding process.
    :return: a string containing the JSON data if *fp* is `None`, otherwise doesn't \
    return anything. If ``validation='lax'`` keyword argument is provided the validation \
    errors are collected and returned, eventually coupled in a tuple with the JSON data.
    :raises: :exc:`XMLSchemaValidationError` if the object is not decodable by \
    the XSD component, or also if it's invalid when ``validation='strict'`` is provided.
    """
    dummy_schema = validation == 'skip'
    source, _schema = get_context(
        xml_document, schema, cls, locations, use_location_hints, dummy_schema, **kwargs
    )
    if json_options is None:
        json_options = {}
    if 'decimal_type' not in kwargs:
        kwargs['decimal_type'] = float

    errors: list[XMLSchemaValidationError] = []

    if path is None and source.is_lazy() and 'cls' not in json_options:
        json_options['cls'] = get_lazy_json_encoder(errors)

    obj = _schema.decode(source, path=path, validation=validation,
                         use_location_hints=use_location_hints, **kwargs)

    if isinstance(obj, tuple):
        errors.extend(obj[1])
        if fp is not None:
            json.dump(obj[0], fp, **json_options)
            return tuple(errors)
        else:
            result = json.dumps(obj[0], **json_options)
            return result, tuple(errors)
    elif fp is not None:
        json.dump(obj, fp, **json_options)
        return None if not errors else tuple(errors)
    else:
        result = json.dumps(obj, **json_options)
        return result if not errors else (result, tuple(errors))


def to_etree(obj: Any,
             schema: Optional[Union[XMLSchemaBase, SchemaSourceType]] = None,
             cls: Optional[Type[XMLSchemaBase]] = None,
             path: Optional[str] = None,
             validation: str = 'strict',
             namespaces: Optional[NsmapType] = None,
             use_defaults: bool = True,
             converter: Optional[ConverterType] = None,
             unordered: bool = False,
             **kwargs: Any) -> EncodeType[ElementType]:
    """
    Encodes a data structure/object to an ElementTree's Element.

    :param obj: the Python object that has to be encoded to XML data.
    :param schema: can be a schema instance or a file-like object or a file path or a URL \
    of a resource or a string containing the schema. If not provided a dummy schema is used.
    :param cls: class to use for building the schema instance (for default uses \
    :class:`XMLSchema10`).
    :param path: is an optional XPath expression for selecting the element of the schema \
    that matches the data that has to be encoded. For default the first global element of \
    the schema is used.
    :param validation: the XSD validation mode. Can be 'strict', 'lax' or 'skip'.
    :param namespaces: is an optional mapping from namespace prefix to URI.
    :param use_defaults: whether to use default values for filling missing data.
    :param converter: an :class:`XMLSchemaConverter` subclass or instance to use for \
    the encoding.
    :param unordered: a flag for explicitly activating unordered encoding mode for \
    content model data. This mode uses content models for a reordered-by-model \
    iteration of the child elements.
    :param kwargs: other optional arguments of :meth:`XMLSchemaBase.iter_encode` and \
    options for the converter.
    :return: An element tree's Element instance. If ``validation='lax'`` keyword argument is \
    provided the validation errors are collected and returned coupled in a tuple with the \
    Element instance.
    :raises: :exc:`XMLSchemaValidationError` if the object is not encodable by the schema, \
    or also if it's invalid when ``validation='strict'`` is provided.
    """
    if cls is None:
        cls = XMLSchema10
    elif not issubclass(cls, XMLSchemaBase):
        raise XMLSchemaTypeError("invalid schema class %r" % cls)

    if schema is None:
        if not path:
            raise XMLSchemaTypeError("without schema a path is required "
                                     "for building a dummy schema")

        if namespaces is None:
            tag = get_extended_qname(path, {'xsd': XSD_NAMESPACE, 'xs': XSD_NAMESPACE})
        else:
            tag = get_extended_qname(path, namespaces)

        if not tag.startswith('{') and ':' in tag:
            raise XMLSchemaTypeError("without schema the path must be "
                                     "mappable to a local or extended name")

        if tag == XSD_SCHEMA:
            assert cls.meta_schema is not None
            _schema = cls.meta_schema
        else:
            _schema = get_dummy_schema(tag, cls)

    elif isinstance(schema, XMLSchemaBase):
        _schema = schema
    else:
        _schema = cls(schema)

    return _schema.encode(
        obj=obj,
        path=path,
        validation=validation,
        namespaces=namespaces,
        use_defaults=use_defaults,
        converter=converter,
        unordered=unordered,
        **kwargs
    )


def from_json(source: Union[str, bytes, IO[str]],
              schema: Optional[Union[XMLSchemaBase, SchemaSourceType]] = None,
              cls: Optional[Type[XMLSchemaBase]] = None,
              path: Optional[str] = None,
              validation: str = 'strict',
              namespaces: Optional[NsmapType] = None,
              use_defaults: bool = True,
              converter: Optional[ConverterType] = None,
              unordered: bool = False,
              json_options: Optional[dict[str, Any]] = None,
              **kwargs: Any) -> EncodeType[ElementType]:
    """
    Deserialize JSON data to an XML Element.

    :param source: can be a string or a :meth:`read()` supporting file-like object \
    containing the JSON document.
    :param schema: an :class:`XMLSchema10` or an :class:`XMLSchema11` instance.
    :param cls: class to use for building the schema instance (for default uses \
    :class:`XMLSchema10`).
    :param path: is an optional XPath expression for selecting the element of the schema \
    that matches the data that has to be encoded. For default the first global element of \
    the schema is used.
    :param validation: the XSD validation mode. Can be 'strict', 'lax' or 'skip'.
    :param namespaces: is an optional mapping from namespace prefix to URI.
    :param use_defaults: whether to use default values for filling missing data.
    :param converter: an :class:`XMLSchemaConverter` subclass or instance to use for \
    the encoding.
    :param unordered: a flag for explicitly activating unordered encoding mode for \
    content model data. This mode uses content models for a reordered-by-model \
    iteration of the child elements.
    :param json_options: a dictionary with options for the JSON deserializer.
    :param kwargs: other optional arguments of :meth:`XMLSchemaBase.iter_encode` and \
    options for converter.
    :return: An element tree's Element instance. If ``validation='lax'`` keyword argument is \
    provided the validation errors are collected and returned coupled in a tuple with the \
    Element instance.
    :raises: :exc:`XMLSchemaValidationError` if the object is not encodable by the schema, \
    or also if it's invalid when ``validation='strict'`` is provided.
    """
    if json_options is None:
        json_options = {}

    if isinstance(source, (str, bytes)):
        obj = json.loads(source, **json_options)
    else:
        obj = json.load(source, **json_options)

    return to_etree(
        obj=obj,
        schema=schema,
        cls=cls,
        path=path,
        validation=validation,
        namespaces=namespaces,
        use_defaults=use_defaults,
        converter=converter,
        unordered=unordered,
        **kwargs
    )


class XmlDocument(XMLResource):
    """
    An XML document bound with its schema. If no schema is get from the provided
    context and validation argument is 'skip' the XML document is associated with
    a generic schema, otherwise a ValueError is raised.

    :param source: a string containing XML data or a file path or a URL or a \
    file like object or an ElementTree or an Element.
    :param schema: can be a :class:`xmlschema.XMLSchema` instance or a file-like \
    object or a file path or a URL of a resource or a string containing the XSD schema.
    :param cls: class to use for building the schema instance (for default \
    :class:`XMLSchema10` is used).
    :param validation: the XSD validation mode to use for validating the XML document, \
    that can be 'strict' (default), 'lax' or 'skip'.
    :param namespaces: is an optional mapping from namespace prefix to URI.
    :param locations: resource location hints, that can be a dictionary or a \
    sequence of couples (namespace URI, resource URL).
    :param use_location_hints: for default, in case a schema instance has \
    to be built, uses also schema locations hints provided within XML data. \
    set this option to `False` to ignore these schema location hints.
    :param kwargs: other optional arguments for building :class:`XMLResource` or \
    :class:`XMLSchema` instances provided as keyword arguments.
    """
    errors: Union[tuple[()], list[XMLSchemaValidationError]] = ()

    def __init__(self, source: XMLSourceType,
                 schema: Optional[Union[XMLSchemaBase, SchemaSourceType]] = None,
                 cls: Optional[Type[XMLSchemaBase]] = None,
                 validation: str = 'strict',
                 namespaces: Optional[NsmapType] = None,
                 locations: Optional[LocationsType] = None,
                 use_location_hints: bool = True,
                 **kwargs: Any) -> None:

        check_validation_mode(validation)
        resource_kwargs = {k: v for k, v in kwargs.items() if k in RESOURCE_KWARGS}
        super().__init__(source, **resource_kwargs)

        self.validation = validation
        self._init_namespaces = get_namespace_map(namespaces)
        self.namespaces = super().get_namespaces(namespaces, root_only=True)

        _self, self.schema = get_context(
            self, schema, cls, locations, use_location_hints,
            dummy_schema=validation == 'skip', **kwargs
        )

        if validation == 'strict':
            self.schema.validate(self, namespaces=self.namespaces)
        elif validation == 'lax':
            self.errors = [e for e in self.schema.iter_errors(self, namespaces=self.namespaces)]
        elif validation != 'skip':
            raise XMLSchemaValueError("%r is not a validation mode" % validation)

    def get_arguments(self) -> dict[str, Any]:
        """Returns keyword arguments for rebuilding the XML document."""
        kwargs = super().get_arguments()
        kwargs.update(
            validation=self.validation,
            schema=self.schema,
            namespaces=self._init_namespaces
        )
        return kwargs

    def get_namespaces(self, namespaces: Optional[NsmapType] = None,
                       root_only: bool = True) -> NsmapType:
        namespaces = get_namespace_map(namespaces)
        update_namespaces(namespaces, self.namespaces.items(), root_declarations=True)
        return super().get_namespaces(namespaces, root_only)

    def getroot(self) -> ElementType:
        """Get the root element of the XML document."""
        return self.root

    def get_etree_document(self) -> Any:
        """
        The resource as ElementTree XML document. If the resource is lazy
        raises a resource error.
        """
        if is_etree_document(self._source):
            return self._source
        elif self._lazy:
            raise XMLResourceError(
                "cannot create an ElementTree instance from a lazy XML resource"
            )
        elif hasattr(self.root, 'nsmap'):
            return self.root.getroottree()  # type: ignore[attr-defined]
        else:
            return ElementTree.ElementTree(self.root)

    def decode(self, **kwargs: Any) -> DecodeType[Any]:
        """
        Decode the XML document to a nested Python dictionary.

        :param kwargs: options for the decode/to_dict method of the schema instance.
        """
        if 'validation' not in kwargs:
            kwargs['validation'] = self.validation
        if 'namespaces' not in kwargs:
            kwargs['namespaces'] = self.namespaces

        obj = self.schema.to_dict(self, **kwargs)
        return obj[0] if isinstance(obj, tuple) else obj

    def to_json(self, fp: Optional[IO[str]] = None,
                json_options: Optional[dict[str, Any]] = None,
                **kwargs: Any) -> JsonDecodeType:
        """
        Converts loaded XML data to a JSON string or file.

        :param fp: can be a :meth:`write()` supporting file-like object.
        :param json_options: a dictionary with options for the JSON deserializer.
        :param kwargs: options for the decode/to_dict method of the schema instance.
        """
        if json_options is None:
            json_options = {}
        path = kwargs.pop('path', None)
        if 'validation' not in kwargs:
            kwargs['validation'] = self.validation
        if 'namespaces' not in kwargs:
            kwargs['namespaces'] = self.namespaces
        if 'decimal_type' not in kwargs:
            kwargs['decimal_type'] = float

        errors: list[XMLSchemaValidationError] = []

        if path is None and self._lazy and 'cls' not in json_options:
            json_options['cls'] = get_lazy_json_encoder(errors)
            kwargs['lazy_decode'] = True

        obj = self.schema.decode(self, path=path, **kwargs)
        if isinstance(obj, tuple):
            if fp is not None:
                json.dump(obj[0], fp, **json_options)
                obj[1].extend(errors)
                return tuple(obj[1])
            else:
                result = json.dumps(obj[0], **json_options)
                obj[1].extend(errors)
                return result, tuple(obj[1])

        elif fp is not None:
            json.dump(obj, fp, **json_options)
            return None if not errors else tuple(errors)
        else:
            result = json.dumps(obj, **json_options)
            return result if not errors else (result, tuple(errors))

    def write(self, file: Union[str, TextIO, BinaryIO],
              encoding: str = 'us-ascii', xml_declaration: bool = False,
              default_namespace: Optional[str] = None, method: str = "xml") -> None:
        """Serialize an XML resource to a file. Cannot be used with lazy resources."""
        if self._lazy:
            raise XMLResourceError("cannot serialize a lazy XML resource")

        kwargs: dict[str, Any] = {
            'xml_declaration': xml_declaration,
            'encoding': encoding,
            'method': method,
        }
        if not default_namespace:
            kwargs['namespaces'] = self.namespaces
        else:
            namespaces: Optional[dict[Optional[str], str]]
            namespaces = {k: v for k, v in self.namespaces.items()}

            if hasattr(self.root, 'nsmap'):
                # noinspection PyTypeChecker
                namespaces[None] = default_namespace
            else:
                namespaces[''] = default_namespace
            kwargs['namespaces'] = namespaces

        _string = etree_tostring(self.root, **kwargs)

        if isinstance(file, str):
            if isinstance(_string, str):
                with open(file, 'w', encoding='utf-8') as fp:
                    fp.write(_string)
            else:
                with open(file, 'wb') as _fp:
                    _fp.write(_string)

        elif isinstance(file, TextIOBase):
            if isinstance(_string, bytes):
                file.write(_string.decode('utf-8'))
            else:
                file.write(_string)

        elif isinstance(file, IOBase):
            if isinstance(_string, str):
                file.write(_string.encode('utf-8'))
            else:
                file.write(_string)
        else:
            msg = "unexpected type %r for 'file' argument"
            raise XMLSchemaTypeError(msg % type(file))