Skip to content

Commit

Permalink
Add multi-source initialization and add_schema() to schema class
Browse files Browse the repository at this point in the history
  • Loading branch information
brunato committed Apr 11, 2021
1 parent 7388310 commit 70af02b
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 7 deletions.
95 changes: 95 additions & 0 deletions tests/validators/test_schema_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,101 @@ def test_listed_and_reversed_elements(self):
elements.reverse()
self.assertListEqual(elements, list(reversed(schema)))

def test_multi_schema_initilization(self):
source1 = dedent("""\
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="elem1"/>
</xs:schema>""")

source2 = dedent("""\
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="elem2"/>
</xs:schema>""")

source3 = dedent("""\
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="elem3"/>
</xs:schema>""")

schema = self.schema_class([source1, source2, source3])
self.assertEqual(len(schema.elements), 3)
self.assertEqual(len(schema.maps.namespaces['']), 3)
self.assertIs(schema.elements['elem1'].schema, schema)
self.assertIs(schema.elements['elem2'].schema, schema.maps.namespaces[''][1])
self.assertIs(schema.elements['elem3'].schema, schema.maps.namespaces[''][2])

with self.assertRaises(XMLSchemaParseError) as ec:
self.schema_class([source1, source2, source2])
self.assertIn("global element with name='elem2' is already defined", str(ec.exception))

source1 = dedent("""\
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://xmlschema.test/ns">
<xs:element name="elem1"/>
</xs:schema>""")

schema = self.schema_class([source1, source2])
self.assertEqual(len(schema.elements), 2)
self.assertEqual(len(schema.maps.namespaces['http://xmlschema.test/ns']), 2)
self.assertIs(schema.elements['elem1'].schema, schema)
self.assertIs(schema.elements['elem2'].schema,
schema.maps.namespaces['http://xmlschema.test/ns'][1])

def test_add_schema(self):
source1 = dedent("""\
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://xmlschema.test/ns">
<xs:element name="elem1"/>
</xs:schema>""")

source2 = dedent("""\
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="elem2"/>
</xs:schema>""")

source3 = dedent("""\
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://xmlschema.test/ns1">
<xs:element name="elem3"/>
</xs:schema>""")

schema = self.schema_class(source1)
schema.add_schema(source2, build=True)
self.assertEqual(len(schema.elements), 1)
self.assertEqual(len(schema.maps.namespaces['http://xmlschema.test/ns']), 1)
self.assertEqual(len(schema.maps.namespaces['']), 1)

# Less checks on duplicate objects for schemas added after the build
schema.add_schema(source2, build=True)
self.assertEqual(len(schema.maps.namespaces['']), 2)
self.assertTrue(schema.maps.built)

with self.assertRaises(XMLSchemaParseError) as ec:
schema.maps.clear()
schema.build()
self.assertIn("global element with name='elem2' is already defined", str(ec.exception))

schema = self.schema_class(source1)
schema.add_schema(source2, namespace='http://xmlschema.test/ns', build=True)
self.assertEqual(len(schema.maps.namespaces['http://xmlschema.test/ns']), 2)

# Need a rebuild to add elem2 from added schema ...
self.assertEqual(len(schema.elements), 1)
schema.maps.clear()
schema.build()
self.assertEqual(len(schema.elements), 2)

# ... so is better to build after sources additions
schema = self.schema_class(source1, build=False)
schema.add_schema(source2, namespace='http://xmlschema.test/ns')
schema.build()
self.assertEqual(len(schema.elements), 2)

# Adding other namespaces do not require rebuild
schema3 = schema.add_schema(source3, build=True)
self.assertEqual(len(schema.maps.namespaces['http://xmlschema.test/ns1']), 1)
self.assertEqual(len(schema3.elements), 1)

def test_export_errors__issue_187(self):
with self.assertRaises(ValueError) as ctx:
self.vh_schema.export(target=self.vh_dir)
Expand Down
7 changes: 5 additions & 2 deletions xmlschema/validators/global_maps.py
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,10 @@ def register(self, schema):
else:
if schema in ns_schemas:
return
elif not any(schema.url == obj.url and schema.__class__ == obj.__class__
elif schema.url is None:
# only by multi-source init or add_schema() by user initiative
ns_schemas.append(schema)
elif not any(schema.url == obj.url and schema.__class__ is obj.__class__
for obj in ns_schemas):
ns_schemas.append(schema)

Expand Down Expand Up @@ -537,7 +540,7 @@ def build(self):
schema.meta_schema = meta_schema
else:
if not self.types and meta_schema.maps is not self:
for source_map, target_map in zip(meta_schema.global_maps, self.global_maps):
for source_map, target_map in zip(meta_schema.maps.global_maps, self.global_maps):
target_map.update(source_map)

not_built_schemas = [schema for schema in self.iter_schemas() if not schema.built]
Expand Down
58 changes: 53 additions & 5 deletions xmlschema/validators/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,13 @@ class XMLSchemaBase(XsdValidator, ValidationMixin, ElementPathMixin, metaclass=X
Base class for an XML Schema instance.
:param source: an URI that reference to a resource or a file path or a file-like \
object or a string containing the schema or an Element or an ElementTree document.
object or a string containing the schema or an Element or an ElementTree document \
or an :class:`XMLRecource` instance. A multi source initialization is supported \
providing a not empty list of XSD sources.
:type source: Element or ElementTree or str or file-like object
:param namespace: is an optional argument that contains the URI of the namespace. \
When specified it must be equal to the *targetNamespace* declared in the schema.
:param namespace: is an optional argument that contains the URI of the namespace \
that has to used in case the schema has no namespace (chameleon schema). For other \
cases, when specified, it must be equal to the *targetNamespace* of the schema.
:type namespace: str or None
:param validation: the XSD validation mode to use for build the schema, \
that can be 'strict' (default), 'lax' or 'skip'.
Expand Down Expand Up @@ -308,6 +311,13 @@ def __init__(self, source, namespace=None, validation='strict', global_maps=None
# Allow sandbox mode without a base_url using the initial schema URL as base
base_url = os.path.dirname(normalize_url(source))

if not isinstance(source, list):
other_sources = None
elif not source:
raise XMLSchemaValueError("no XSD source provided!")
else:
source, other_sources = source[0], source[1:]

if isinstance(source, XMLResource):
self.source = source
else:
Expand Down Expand Up @@ -449,6 +459,17 @@ def __init__(self, source, namespace=None, validation='strict', global_maps=None
self.default_open_content = XsdDefaultOpenContent(child, self)
break

if other_sources:
for _source in other_sources:
if not isinstance(_source, XMLResource):
_source = XMLResource(_source, base_url, allow, defuse, timeout)

if not _source.root.get('targetNamespace') and self.target_namespace:
# Adding a chameleon schema: set the namespace with targetNamespace
self.add_schema(_source, namespace=self.target_namespace)
else:
self.add_schema(_source)

try:
if build:
self.maps.build()
Expand Down Expand Up @@ -1035,12 +1056,13 @@ def _parse_inclusions(self):
else:
schema.redefine = self

def include_schema(self, location, base_url=None):
def include_schema(self, location, base_url=None, build=False):
"""
Includes a schema for the same namespace, from a specific URL.
:param location: is the URL of the schema.
:param base_url: is an optional base URL for fetching the schema resource.
:param build: defines when to build the imported schema, the default is to not build.
:return: the included :class:`XMLSchema` instance.
"""
schema_url = fetch_resource(location, base_url)
Expand All @@ -1060,7 +1082,7 @@ def include_schema(self, location, base_url=None):
allow=self.allow,
defuse=self.defuse,
timeout=self.timeout,
build=False,
build=build,
)

if schema is self:
Expand Down Expand Up @@ -1208,6 +1230,32 @@ def import_schema(self, namespace, location, base_url=None, force=False, build=F
self.imports[namespace] = schema
return schema

def add_schema(self, source, namespace=None, build=False):
"""
Add another schema source to the maps of the instance.
:param source: an URI that reference to a resource or a file path or a file-like \
object or a string containing the schema or an Element or an ElementTree document.
:param namespace: is an optional argument that contains the URI of the namespace \
that has to used in case the schema has no namespace (chameleon schema). For other \
cases, when specified, it must be equal to the *targetNamespace* of the schema.
:param build: defines when to build the imported schema, the default is to not build.
:return: the added :class:`XMLSchema` instance.
"""
return type(self)(
source=source,
namespace=namespace,
validation=self.validation,
global_maps=self.maps,
converter=self.converter,
locations=self._locations,
base_url=self.base_url,
allow=self.allow,
defuse=self.defuse,
timeout=self.timeout,
build=build,
)

def export(self, target, save_remote=False):
"""
Exports a schema instance. The schema instance is exported to a
Expand Down

0 comments on commit 70af02b

Please sign in to comment.