diff --git a/README.rst b/README.rst index a7a32e4..5739465 100644 --- a/README.rst +++ b/README.rst @@ -163,6 +163,7 @@ version 0.7 (development) providing the initial fix. * Fixed loading recursive WSDL imports. +* Fixed loading recursive XSD imports/includes. * Made ``suds`` no longer eat up, log & ignore exceptions raised from registered user plugins (detected & reported by Ezequiel Ruiz & Bouke Haarsma, patch & test case contributed by Bouke Haarsma). @@ -415,10 +416,6 @@ version 0.7 (development) related ``pytest`` ``xdist`` usage problems, discovering & explaining the underlying issue as well as providing an initial project patch for it. -* Known defects. - - * Loading recursive XSD imports/includes is broken. - version 0.6 (2014-01-24) ------------------------- diff --git a/TODO.txt b/TODO.txt index 82707a7..c0c9799 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1501,9 +1501,15 @@ PRIORITIZED: (30.06.2015.) - * (Jurko) Fix recursive XSD import issue. - * Add tests. - * Fix issue. +(+) * (Jurko) Review and commit unpublished cleanup work on Jurko's machine. + +(01.06.2015.) + +(+) * (Jurko) Fix recursive XSD import issue. +(+) * Add tests. +(+) * Fix issue. + +(02.06.2015.) * (Jurko) Review and commit unpublished cleanup work on Jurko's machine. diff --git a/suds/wsdl.py b/suds/wsdl.py index d50c872..6f194d6 100644 --- a/suds/wsdl.py +++ b/suds/wsdl.py @@ -239,16 +239,17 @@ class Definitions(WObject): def build_schema(self): """Process L{Types} objects and create the schema collection.""" + loaded_schemata = {} container = SchemaCollection(self) for t in (t for t in self.types if t.local()): for root in t.contents(): - schema = Schema(root, self.url, self.options, container) + schema = Schema(root, self.url, self.options, loaded_schemata, container) container.add(schema) if not container: root = Element.buildPath(self.root, "types/schema") - schema = Schema(root, self.url, self.options, container) + schema = Schema(root, self.url, self.options, loaded_schemata, container) container.add(schema) - self.schema = container.load(self.options) + self.schema = container.load(self.options, loaded_schemata) #TODO: Recheck this XSD schema merging. XSD schema imports are not # supposed to be transitive. They only allow the importing schema to # reference entities from the imported schema, but do not include them diff --git a/suds/xsd/schema.py b/suds/xsd/schema.py index 0945d27..d057024 100644 --- a/suds/xsd/schema.py +++ b/suds/xsd/schema.py @@ -80,7 +80,7 @@ class SchemaCollection(UnicodeMixin): existing.root.children += schema.root.children existing.root.nsprefixes.update(schema.root.nsprefixes) - def load(self, options): + def load(self, options, loaded_schemata): """ Load schema objects for the root nodes. - de-reference schemas @@ -88,6 +88,8 @@ class SchemaCollection(UnicodeMixin): @param options: An options dictionary. @type options: L{options.Options} + @param loaded_schemata: Already loaded schemata cache (URL --> Schema). + @type loaded_schemata: dict @return: The merged schema. @rtype: L{Schema} @@ -97,7 +99,7 @@ class SchemaCollection(UnicodeMixin): for child in self.children: child.build() for child in self.children: - child.open_imports(options) + child.open_imports(options, loaded_schemata) for child in self.children: child.dereference() log.debug("loaded:\n%s", self) @@ -199,7 +201,8 @@ class Schema(UnicodeMixin): Tag = "schema" - def __init__(self, root, baseurl, options, container=None): + def __init__(self, root, baseurl, options, loaded_schemata=None, + container=None): """ @param root: The XML root. @type root: L{sax.element.Element} @@ -207,6 +210,9 @@ class Schema(UnicodeMixin): @type baseurl: basestring @param options: An options dictionary. @type options: L{options.Options} + @param loaded_schemata: An optional already loaded schemata cache (URL + --> Schema). + @type loaded_schemata: dict @param container: An optional container. @type container: L{SchemaCollection} @@ -237,6 +243,9 @@ class Schema(UnicodeMixin): # really necessary or if we can simply build our top-level WSDL # contained schemata one by one as they are loaded if container is None: + if loaded_schemata is None: + loaded_schemata = {} + loaded_schemata[baseurl] = self #TODO: It seems like this build() step can be done for each schema # on its own instead of letting the container do it. Building our # XSD schema objects should not require any external schema @@ -249,7 +258,7 @@ class Schema(UnicodeMixin): # get built, but there is bound to be a cleaner way to do this, # similar to how we support such XML modifications in suds plugins. self.build() - self.open_imports(options) + self.open_imports(options, loaded_schemata) log.debug("built:\n%s", self) self.dereference() log.debug("dereferenced:\n%s", self) @@ -326,7 +335,7 @@ class Schema(UnicodeMixin): schema.merged = True return self - def open_imports(self, options): + def open_imports(self, options, loaded_schemata): """ Instruct all contained L{sxbasic.Import} children to import all of their referenced schemas. The imported schema contents are I{merged} @@ -334,13 +343,15 @@ class Schema(UnicodeMixin): @param options: An options dictionary. @type options: L{options.Options} + @param loaded_schemata: Already loaded schemata cache (URL --> Schema). + @type loaded_schemata: dict """ for imp in self.imports: - imported = imp.open(options) + imported = imp.open(options, loaded_schemata) if imported is None: continue - imported.open_imports(options) + imported.open_imports(options, loaded_schemata) log.debug("imported:\n%s", imported) self.merge(imported) @@ -414,7 +425,7 @@ class Schema(UnicodeMixin): except Exception: return False - def instance(self, root, baseurl, options): + def instance(self, root, baseurl, loaded_schemata, options): """ Create and return an new schema object using the specified I{root} and I{URL}. @@ -423,6 +434,8 @@ class Schema(UnicodeMixin): @type root: L{sax.element.Element} @param baseurl: A base URL. @type baseurl: str + @param loaded_schemata: Already loaded schemata cache (URL --> Schema). + @type loaded_schemata: dict @param options: An options dictionary. @type options: L{options.Options} @return: The newly created schema object. @@ -430,7 +443,7 @@ class Schema(UnicodeMixin): @note: This is only used by Import children. """ - return Schema(root, baseurl, options) + return Schema(root, baseurl, options, loaded_schemata) def str(self, indent=0): tab = "%*s" % (indent * 3, "") diff --git a/suds/xsd/sxbasic.py b/suds/xsd/sxbasic.py index 19a9078..0f659ba 100644 --- a/suds/xsd/sxbasic.py +++ b/suds/xsd/sxbasic.py @@ -549,12 +549,14 @@ class Import(SchemaObject): self.location = self.locations.get(self.ns[1]) self.opened = False - def open(self, options): + def open(self, options, loaded_schemata): """ Open and import the referenced schema. @param options: An options dictionary. @type options: L{options.Options} + @param loaded_schemata: Already loaded schemata cache (URL --> Schema). + @type loaded_schemata: dict @return: The referenced schema. @rtype: L{Schema} @@ -569,7 +571,11 @@ class Import(SchemaObject): if self.location is None: log.debug("imported schema (%s) not-found", self.ns[1]) else: - result = self.__download(options) + url = self.location + if "://" not in url: + url = urljoin(self.schema.baseurl, url) + result = (loaded_schemata.get(url) or + self.__download(url, loaded_schemata, options)) log.debug("imported:\n%s", result) return result @@ -578,17 +584,14 @@ class Import(SchemaObject): if self.ns[1] != self.schema.tns[1]: return self.schema.locate(self.ns) - def __download(self, options): + def __download(self, url, loaded_schemata, options): """Download the schema.""" - url = self.location try: - if "://" not in url: - url = urljoin(self.schema.baseurl, url) reader = DocumentReader(options) d = reader.open(url) root = d.root() root.set("url", url) - return self.schema.instance(root, url, options) + return self.schema.instance(root, url, loaded_schemata, options) except TransportError: msg = "import schema (%s) at (%s), failed" % (self.ns[1], url) log.error("%s, %s", self.id, msg, exc_info=True) @@ -618,12 +621,14 @@ class Include(SchemaObject): self.location = self.locations.get(self.ns[1]) self.opened = False - def open(self, options): + def open(self, options, loaded_schemata): """ Open and include the referenced schema. @param options: An options dictionary. @type options: L{options.Options} + @param loaded_schemata: Already loaded schemata cache (URL --> Schema). + @type loaded_schemata: dict @return: The referenced schema. @rtype: L{Schema} @@ -632,22 +637,23 @@ class Include(SchemaObject): return self.opened = True log.debug("%s, including location='%s'", self.id, self.location) - result = self.__download(options) + url = self.location + if "://" not in url: + url = urljoin(self.schema.baseurl, url) + result = (loaded_schemata.get(url) or + self.__download(url, loaded_schemata, options)) log.debug("included:\n%s", result) return result - def __download(self, options): + def __download(self, url, loaded_schemata, options): """Download the schema.""" - url = self.location try: - if "://" not in url: - url = urljoin(self.schema.baseurl, url) reader = DocumentReader(options) d = reader.open(url) root = d.root() root.set("url", url) self.__applytns(root) - return self.schema.instance(root, url, options) + return self.schema.instance(root, url, loaded_schemata, options) except TransportError: msg = "include schema at (%s), failed" % url log.error("%s, %s", self.id, msg, exc_info=True) diff --git a/tests/test_suds.py b/tests/test_suds.py index 87baa46..11c3799 100644 --- a/tests/test_suds.py +++ b/tests/test_suds.py @@ -2030,7 +2030,6 @@ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"> ("DoesNotExist", "OMG")) -@pytest.mark.xfail def test_recursive_XSD_import(): url_xsd = "suds://xsd" xsd = b("""\