diff --git a/CHANGES b/CHANGES index 95e55c4..8bbebc5 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,119 @@ +2.1.1 (2017-06-11) +------------------ + - Fix previous release, it contained an incorrect dependency (Mock 2.1.) due + to bumpversion :-( + + +2.1.0 (2017-06-11) +------------------ + - Fix recursion error while creating the signature for a global element when + it references itself (via ref attribute). + - Update Client.create_message() to apply plugins and wsse (#465) + - Fix handling unknown xsi types when parsing elements using xsd:anyType (#455) + + +2.0.0 (2017-05-22) +------------------ + +This is a major release, and contains a number of backwards incompatible +changes to the API. + + - Default values of optional elements are not set by default anymore (#423) + - Refactor the implementation of wsdl:arrayType too make the API more + pythonic (backwards incompatible). + - The call signature for Client.create_message() was changed. It now requires + the service argument: + + ``Client.create_message(service, operation_name, *args, **kwargs)`` + + - Choice elements now only work with keyword arguments and raise an exception + if positional arguments are passed (#439) + - Implement initial multiref support for SOAP RPC (#326). This was done using + really good real-world tests from vstoykov (thanks!) + - Fix exception on empty SOAP response (#442) + - Fix XSD default values for boolean types (Bartek Wójcicki, #386) + + +1.6.0 (2017-04-27) +------------------ + - Implement ValueObject.__json__ for json serialization (#258) + - Improve handling of unexpected elements for soap:header (#378) + - Accept unexpected elements in complexTypes when strict is False + - Fix elementFormDefault/attributeFormDefault for xsd:includes (#426) + + +1.5.0 (2017-04-22) +------------------ + - Fix issue where values of indicators (sequence/choice/all) would + write to the same internal dict. (#425) + - Set default XML parse mode to strict as was intended (#332) + - Add support for pickling value objects (#417) + - Add explicit Nil value via ``zeep.xsd.Nil`` (#424) + - Add xml_huge_tree kwarg to the Client() to enable lxml's huge_tree mode, + this is disabled by default (#332) + - Add support to pass base-types to type extensions (#416) + - Handle wsdl errors more gracefully by disabling the invalid operations + instead of raising an exception (#407, #387) + + +1.4.1 (2017-04-01) +------------------ + - The previous release (1.4.0) contained an incorrect dependency due to + bumpversion moving all 1.3.0 versions to 1.4.0. This fixes it. + + +1.4.0 (2017-04-01) +------------------ + - Hardcode the xml prefix to the xml namespace as defined in the specs (#367) + - Fix parsing of unbound sequences within xsd choices (#380) + - Use logger.debug() for debug related logging (#369) + - Add the ``Client.raw_response`` option to let zeep return the raw + transport response (requests.Response) instead of trying to parse it. + - Handle minOccurs/maxOccurs properlhy for xsd:Group elements. This also + fixes a bug in the xsd:Choice handling for multiple elements (#374, #410) + - Fix raising XMLSyntaxError when loading invalid XML (Antanas Sinica, #396) + + +1.3.0 (2017-03-14) +------------------ + - Add support for nested xsd:choice elements (#370) + - Fix unresolved elements for xsd:extension, this was a regression introduced + in 1.2.0 (#377) + + +1.2.0 (2017-03-12) +------------------ + - Add flag to disable strict mode in the Client. This allows zeep to better + work with non standard compliant SOAP Servers. See the documentation for + usage and potential downsides. + - Minor refactor of resolving of elements for improved performance + - Support the SOAP 1.2 'http://www.w3.org/2003/05/soap/bindings/HTTP/' + transport uri (#355) + - Fallback to matching wsdl lookups to matching when the target namespace is + empty (#356) + - Improve the handling of xsd:includes, the default namespace of the parent + schema is now also used during resolving of the included schema. (#360) + - Properly propagate the global flag for complex types when an xsd:restriction + is used (#360) + - Filter out duplicate types and elements when dump the wsdl schema (#360) + - Add ``zeep.CachingClient()`` which enables the SqliteCache by default + + +1.1.0 (2017-02-18) +------------------ + - Fix an attribute error when an complexType used xsd:anyType as base + restriction (#352) + - Update asyncio transport to return requests.Response objects (#335) + + +1.0.0 (2017-01-31) +------------------ + - Use cgi.parse_header() to extract media_type for multipart/related checks + (#327) + - Don't ignore nil elements, instead return None when parsing xml (#328) + - Fix regression when using WSA with an older lxml version (#197) + + 0.27.0 (2017-01-28) ------------------- - Add support for SOAP attachments (multipart responses). (Dave Wapstra, #302) @@ -11,13 +127,13 @@ This release again introduces some backwords incompatibilties. The next release will hopefully be 1.0 which will introduce semver. - - **backwards-incompatible**: The Transport class now accepts a + - **backwards-incompatible**: The Transport class now accepts a ``requests.Session()`` object instead of ``http_auth`` and ``verify``. This allows for more flexibility. - **backwards-incompatible**: Zeep no longer sets a default cache backend. Please see http://docs.python-zeep.org/en/master/transport.html#caching for information about how to configure a cache. - - Add ``zeep.xsd.SkipValue`` which instructs the serialize to ignore the + - Add ``zeep.xsd.SkipValue`` which instructs the serialize to ignore the element. - Support duplicate target namespaces in the wsdl definition (#320) - Fix resolving element/types for xsd schema's with duplicate tns (#319) @@ -40,12 +156,12 @@ will hopefully be 1.0 which will introduce semver. type. Instead log a message (#273) - Fix serializing etree.Element instances in the helpers.serialize function (#255) - - Fix infinite loop during parsing of xsd.Sequence where max_occurs is + - Fix infinite loop during parsing of xsd.Sequence where max_occurs is unbounded (#256) - Make the xsd.Element name kwarg required - - Improve handling of the xsd:anyType element when passed instances of + - Improve handling of the xsd:anyType element when passed instances of complexType's (#252) - - Silently ignore unsupported binding transports instead of an hard error + - Silently ignore unsupported binding transports instead of an hard error (#277) - Support microseconds for xsd.dateTime and xsd.Time (#280) - Don't mutate passed values to the zeep operations (#280) @@ -56,7 +172,7 @@ will hopefully be 1.0 which will introduce semver. - Add Client.set_default_soapheaders() to set soapheaders which are to be used on all operations done via the client object. - Add basic support for asyncio using aiohttp. Many thanks to chrisimcevoy - for the initial implementation! Please see + for the initial implementation! Please see https://github.com/mvantellingen/python-zeep/pull/207 and https://github.com/mvantellingen/python-zeep/pull/251 for more information - Fix recursion error when generating the call signature (jaceksnet, #264) @@ -65,7 +181,7 @@ will hopefully be 1.0 which will introduce semver. 0.22.1 (2016-11-22) ------------------- - Fix reversed() error (jaceksnet) (#260) - - Better error message when unexpected xml elements are encountered in + - Better error message when unexpected xml elements are encountered in sequences. @@ -74,13 +190,13 @@ will hopefully be 1.0 which will introduce semver. - Force the soap:address / http:address to HTTPS when the wsdl is loaded from a https url (#228) - Improvements to the xsd:union handling. The matching base class is now used - for serializing/deserializing the values. If there is no matching base class + for serializing/deserializing the values. If there is no matching base class then the raw value is returned. (#195) - Fix handling of xsd:any with maxOccurs > 1 in xsd:choice elements (#253) - - Add workaround for schema's importing the xsd from + - Add workaround for schema's importing the xsd from http://www.w3.org/XML/1998/namespace (#220) - Add new Client.type_factory(namespace) method which returns a factory to - simplify creation of types. + simplify creation of types. @@ -88,15 +204,15 @@ will hopefully be 1.0 which will introduce semver. ------------------- - Don't error on empty xml namespaces declarations in inline schema's (#186) - Wrap importing of sqlite3 in try..except for Google App Engine (#243) - - Don't use pkg_resources to determine the zeep version, use __version__ + - Don't use pkg_resources to determine the zeep version, use __version__ instead (#243). - - Fix SOAP arrays by wrapping children in the appropriate element + - Fix SOAP arrays by wrapping children in the appropriate element (joeribekker, #236) - Add ``operation_timeout`` kwarg to the Transport class to set timeouts for operations. The default is still no timeout (#140) - Introduce client.options context manager to temporarily override various options (only timeout for now) (#140) - - Wrap the parsing of xml values in a try..except block and log an error + - Wrap the parsing of xml values in a try..except block and log an error instead of throwing an exception (#137) - Fix xsd:choice xml rendering with nested choice/sequence structure (#221) - Correctly resolve header elements of which the message part defines the @@ -106,7 +222,7 @@ will hopefully be 1.0 which will introduce semver. 0.20.0 (2016-10-24) ------------------- - Major performance improvements / lower memory usage. Zeep now no longer - copies data and alters it in place but instead uses a set to keep track of + copies data and alters it in place but instead uses a set to keep track of modified data. - Fix parsing empty soap response (#223) - Major refactor of the xsd:extension / xsd:restriction implementation. @@ -118,12 +234,12 @@ will hopefully be 1.0 which will introduce semver. ------------------- - **backwards-incompatible**: If the WSDL defines that the endpoint returns soap:header elements and/or multple soap:body messages then the return - signature of the operation is changed. You can now explcitly access the + signature of the operation is changed. You can now explcitly access the body and header elements. - Fix parsing HTTP bindings when there are no message elements (#185) - Fix deserializing RPC responses (#219 - Add support for SOAP 1.2 Fault subcodes (#210, vashek) - - Don't alter the _soapheaders elements during rendering, instead create a + - Don't alter the _soapheaders elements during rendering, instead create a deepcopy first. (#188) - Add the SOAPAction to the Content-Type header in SOAP 1.2 bindings (#211) - Fix issue when mixing elements and any elements in a choice type (#192) @@ -139,7 +255,7 @@ will hopefully be 1.0 which will introduce semver. 0.18.0 (2016-09-23) ------------------- - - Fix parsing Any elements by using the namespace map of the response node + - Fix parsing Any elements by using the namespace map of the response node instead of the namespace map of the wsdl. (#184, #164) - Improve handling of nested choice elements (choice>sequence>choice) @@ -149,14 +265,14 @@ will hopefully be 1.0 which will introduce semver. - Add support for xsd:notation (#183) - Add improvements to resolving phase so that all objects are resolved. - Improve implementation of xsd.attributeGroup and xsd.UniqueType - - Create a deepcopy of the args and kwargs passed to objects so that the + - Create a deepcopy of the args and kwargs passed to objects so that the original are unmodified. - Improve handling of wsdl:arrayType 0.16.0 (2016-09-06) ------------------- - - Fix error when rendering choice elements with have sequences as children, + - Fix error when rendering choice elements with have sequences as children, see #150 - Re-use credentials passed to python -mzeep (#130) - Workaround invalid usage of qualified vs non-qualified element tags in the @@ -183,7 +299,7 @@ will hopefully be 1.0 which will introduce semver. 0.14.0 (2016-08-03) ------------------- - Global attributes are now always correctly handled as qualified. (#129) - - Fix parsing xml data containing simpleContent types (#136). + - Fix parsing xml data containing simpleContent types (#136). - Set xsi:nil attribute when serializing objects to xml (#141) - Fix rendering choice elements when the element is mixed with other elements in a sequence (#150) @@ -199,8 +315,8 @@ will hopefully be 1.0 which will introduce semver. exception. This better matches with what lxml does. - **backwards-incompatible**: The ``persistent`` kwarg is removed from the SqliteCache.__init__() call. Use the new InMemoryCache() instead when you - don't want to persist data. This was required to make the SqliteCache - backend thread-safe since we now open/close the db when writing/reading + don't want to persist data. This was required to make the SqliteCache + backend thread-safe since we now open/close the db when writing/reading from it (with an additional lock). - Fix zeep.helpers.serialize_object() for nested objects (#123) - Remove fallback between soap 1.1 and soap 1.2 namespaces during the parsing @@ -209,30 +325,30 @@ will hopefully be 1.0 which will introduce semver. 0.12.0 (2016-07-09) ------------------- - - **backwards-incompatible**: Choice elements are now unwrapped if + - **backwards-incompatible**: Choice elements are now unwrapped if maxOccurs=1. This results in easier operation definitions when choices are - used. + used. - **backwards-incompatible**: The _soapheader kwarg is renamed to _soapheaders and now requires a nested dictionary with the header name as key or a list - of values (value object or lxml.etree.Element object). Please see the + of values (value object or lxml.etree.Element object). Please see the call signature of the function using ``python -mzeep ``. - Support the element ref's to xsd:schema elements. - Improve the signature() output of element and type definitions - Accept lxml.etree.Element objects as value for Any elements. - And various other fixes - + 0.11.0 (2016-07-03) ------------------- - **backwards-incompatible**: The kwarg name for Any and Choice elements are renamed to generic ``_value_N`` names. - - **backwards-incompatible**: Client.set_address() is replaced with the + - **backwards-incompatible**: Client.set_address() is replaced with the Client.create_service() call - Auto-load the http://schemas.xmlsoap.org/soap/encoding/ schema if it is referenced but not imported. Too many XSD's assume that the schema is always available. - - Major refactoring of the XSD handling to correctly support nested - xsd:sequence elements. + - Major refactoring of the XSD handling to correctly support nested + xsd:sequence elements. - Add ``logger.debug()`` calls around Transport.post() to allow capturing the content send/received from the server - Add proper support for default values on attributes and elements. @@ -240,12 +356,12 @@ will hopefully be 1.0 which will introduce semver. 0.10.0 (2016-06-22) ------------------- - - Make global elements / types truly global by refactoring the Schema + - Make global elements / types truly global by refactoring the Schema parsing. Previously the lookups where non-transitive, but this should only be the case during parsing of the xml schema. - Properly unwrap XML responses in soap.DocumentMessage when a choice is the root element. (#80) - - Update exceptions structure, all zeep exceptions are now using + - Update exceptions structure, all zeep exceptions are now using zeep.exceptions.Error() as base class. @@ -253,12 +369,12 @@ will hopefully be 1.0 which will introduce semver. ------------------ - Quote the SOAPAction header value (Derek Harland) - Undo fallback for SOAPAction if it is empty (#83) - + 0.9.0 (2016-06-14) ------------------ - Use the appdirs module to retrieve the OS cache path. Note that this results - in an other default cache path then previous releases! See + in an other default cache path then previous releases! See https://github.com/ActiveState/appdirs for more information. - Fix regression when initializing soap objects with invalid kwargs. - Update wsse.UsernameToken to set encoding type on nonce (Antonio Cuni) @@ -273,7 +389,7 @@ will hopefully be 1.0 which will introduce semver. 0.8.1 (2016-06-08) ------------------ - - Use the operation name for the xml element which wraps the parameters in + - Use the operation name for the xml element which wraps the parameters in for soap RPC messages (#60) @@ -281,7 +397,7 @@ will hopefully be 1.0 which will introduce semver. ------------------ - Add ability to override the soap endpoint via `Client.set_address()` - Fix parsing ComplexTypes which have no child elements (#50) - - Handle xsi:type attributes on anyType's correctly when deserializing + - Handle xsi:type attributes on anyType's correctly when deserializing responses (#17) - Fix xsd:restriction on xsd:simpleType's when the base type wasn't defined yet. (#59) @@ -298,9 +414,9 @@ will hopefully be 1.0 which will introduce semver. ------------------ - Add support HTTP authentication (mcordes). This adds a new attribute to the Transport client() which passes the http_auth value to requests. (#31) - - Fix issue where setting cache=None to Transport class didn't disable + - Fix issue where setting cache=None to Transport class didn't disable caching. - - Refactor handling of wsdl:imports, don't merge definitions but instead + - Refactor handling of wsdl:imports, don't merge definitions but instead lookup values in child definitions. (#40) - Remove unused namespace declarations from the generated SOAP messages. - Update requirement of six>=1.0.0 to six>=1.9.0 (#39) @@ -323,14 +439,14 @@ will hopefully be 1.0 which will introduce semver. ------------------ - Handle attributes during parsing of the response values> - Don't create empty soap objects when the root element is empty. - - Implement support for WSSE usernameToken profile including + - Implement support for WSSE usernameToken profile including passwordText/passwordDigest. - Improve XSD date/time related builtins. - - Various minor XSD handling fixes + - Various minor XSD handling fixes - Use the correct soap-envelope XML namespace for the Soap 1.2 binding - Use `application/soap+xml` as content-type in the Soap 1.2 binding - - **backwards incompatible**: Make cache part of the transport object - instead of the client. This changes the call signature of the Client() + - **backwards incompatible**: Make cache part of the transport object + instead of the client. This changes the call signature of the Client() class. (Marek Wywiał) - Add the `verify` kwarg to the Transport object to disable ssl certificate verification. (Marek Wywiał) @@ -361,7 +477,7 @@ will hopefully be 1.0 which will introduce semver. ------------------ - Improve xsd.DateTime, xsd.Date and xsd.Time implementations by using the isodate module. - - Implement xsd.Duration + - Implement xsd.Duration 0.2.3 (2016-04-03) diff --git a/CONTRIBUTORS.rst b/CONTRIBUTORS.rst new file mode 100644 index 0000000..44a9546 --- /dev/null +++ b/CONTRIBUTORS.rst @@ -0,0 +1,45 @@ +Authors +======= +* Michael van Tellingen + +Contributors +============ +* vashek +* Marco Vellinga +* jaceksnet +* Andrew Serong +* Joeri Bekker +* Eric Wong +* Jacek Stępniewski +* Alexey Stepanov +* Julien Delasoie +* bjarnagin +* mcordes +* Sam Denton +* David Baumgold +* fiebiga +* Antonio Cuni +* Alexandre de Mari +* Jason Vertrees +* Nicolas Evrard +* Matt Grimm (mgrimm) +* Marek Wywiał +* Falldog +* btmanm +* Caleb Salt +* Julien Marechal +* Mike Fiedler +* Dave Wapstra +* OrangGeeGee +* Stefano Parmesan +* Jan Murre +* Ben Tucker +* Bruno Duyé +* Christoph Heuel +* Derek Harland +* Eric Waller +* Falk Schuetzenmeister +* Jon Jenkins +* Raymond Piller +* Zoltan Benedek +* Øyvind Heddeland Instefjord diff --git a/LICENSE b/LICENSE index 75eecda..1229d20 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2016 Michael van Tellingen +Copyright (c) 2016-2017 Michael van Tellingen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..2f6e6ce --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,19 @@ +# Exclude everything by default +exclude * +recursive-exclude * * + +include MANIFEST.in +include CHANGES +include CONTRIBUTORS.rst +include LICENSE +include README.rst +include setup.cfg +include setup.py + +graft examples +graft src +graft tests + +global-exclude __pycache__ +global-exclude *.py[co] +global-exclude .DS_Store diff --git a/PKG-INFO b/PKG-INFO index 0dcc777..6f21657 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: zeep -Version: 0.27.0 +Version: 2.1.1 Summary: A modern/fast Python SOAP client based on lxml / requests Home-page: http://docs.python-zeep.org Author: Michael van Tellingen @@ -57,18 +57,16 @@ Description: ======================== Support ======= - If you encounter bugs then please `let me know`_ . A copy of the WSDL file if - possible would be most helpful. + If you want to report a bug then please first read + http://docs.python-zeep.org/en/master/reporting_bugs.html I'm also able to offer commercial support. As in contracting work. Please - contact me at info@mvantellingen.nl for more information. If you just have a - random question and don't intent to actually pay me for my support then please - DO NOT email me at that e-mail address but just use stackoverflow or something.. - - .. _let me know: https://github.com/mvantellingen/python-zeep/issues + contact me at info@mvantellingen.nl for more information. Note that asking + questions or reporting bugs via this e-mail address will be ignored. Pleae use + the appropriate channels for that (e.g. stackoverflow) Platform: UNKNOWN -Classifier: Development Status :: 4 - Beta +Classifier: Development Status :: 5 - Production/Stable Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 diff --git a/README.rst b/README.rst index c71c9c6..647b78c 100644 --- a/README.rst +++ b/README.rst @@ -72,12 +72,10 @@ information. Support ======= -If you encounter bugs then please `let me know`_ . A copy of the WSDL file if -possible would be most helpful. +If you want to report a bug then please first read +http://docs.python-zeep.org/en/master/reporting_bugs.html I'm also able to offer commercial support. As in contracting work. Please -contact me at info@mvantellingen.nl for more information. If you just have a -random question and don't intent to actually pay me for my support then please -DO NOT email me at that e-mail address but just use stackoverflow or something.. - -.. _let me know: https://github.com/mvantellingen/python-zeep/issues +contact me at info@mvantellingen.nl for more information. Note that asking +questions or reporting bugs via this e-mail address will be ignored. Pleae use +the appropriate channels for that (e.g. stackoverflow) diff --git a/examples/http_basic_auth.py b/examples/http_basic_auth.py index e48f6b5..0fe84dd 100644 --- a/examples/http_basic_auth.py +++ b/examples/http_basic_auth.py @@ -1,10 +1,16 @@ from __future__ import print_function + +from requests import Session +from requests.auth import HTTPBasicAuth + import zeep from zeep.transports import Transport # Example using basic authentication with a webservice -transport_with_basic_auth = Transport(http_auth=('username', 'password')) +session = Session() +session.auth = HTTPBasicAuth('username', 'password') +transport_with_basic_auth = Transport(session=session) client = zeep.Client( wsdl='http://nonexistent?WSDL', diff --git a/setup.cfg b/setup.cfg index 3128c8e..e77a765 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.27.0 +current_version = 2.1.1 commit = true tag = true tag_name = {new_version} @@ -19,6 +19,8 @@ max-line-length = 99 [bumpversion:file:docs/conf.py] +[bumpversion:file:docs/index.rst] + [bumpversion:file:src/zeep/__init__.py] [coverage:run] @@ -38,5 +40,4 @@ show_missing = True [egg_info] tag_build = tag_date = 0 -tag_svn_revision = 0 diff --git a/setup.py b/setup.py index d75137a..dff575e 100755 --- a/setup.py +++ b/setup.py @@ -1,15 +1,16 @@ import re +import sys from setuptools import find_packages, setup install_requires = [ 'appdirs>=1.4.0', - 'cached-property>=1.0.0', + 'cached-property>=1.3.0', 'defusedxml>=0.4.1', 'isodate>=0.5.4', 'lxml>=3.0.0', 'requests>=2.7.0', - 'requests-toolbelt>=0.7.0', + 'requests-toolbelt>=0.7.1', 'six>=1.9.0', 'pytz', ] @@ -18,9 +19,7 @@ docs_require = [ 'sphinx>=1.4.0', ] -async_require = [ - 'aiohttp>=1.0', -] +async_require = [] # see below xmlsec_require = [ 'xmlsec>=0.6.1', @@ -41,13 +40,19 @@ tests_require = [ 'flake8-debugger==1.4.0', ] + +if sys.version_info > (3, 4, 2): + async_require.append('aiohttp>=1.0') + tests_require.append('aioresponses>=0.1.3') + + with open('README.rst') as fh: long_description = re.sub( '^.. start-no-pypi.*^.. end-no-pypi', '', fh.read(), flags=re.M | re.S) setup( name='zeep', - version='0.27.0', + version='2.1.1', description='A modern/fast Python SOAP client based on lxml / requests', long_description=long_description, author="Michael van Tellingen", @@ -69,7 +74,7 @@ setup( license='MIT', classifiers=[ - 'Development Status :: 4 - Beta', + 'Development Status :: 5 - Production/Stable', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', diff --git a/src/zeep.egg-info/PKG-INFO b/src/zeep.egg-info/PKG-INFO index 0dcc777..6f21657 100644 --- a/src/zeep.egg-info/PKG-INFO +++ b/src/zeep.egg-info/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: zeep -Version: 0.27.0 +Version: 2.1.1 Summary: A modern/fast Python SOAP client based on lxml / requests Home-page: http://docs.python-zeep.org Author: Michael van Tellingen @@ -57,18 +57,16 @@ Description: ======================== Support ======= - If you encounter bugs then please `let me know`_ . A copy of the WSDL file if - possible would be most helpful. + If you want to report a bug then please first read + http://docs.python-zeep.org/en/master/reporting_bugs.html I'm also able to offer commercial support. As in contracting work. Please - contact me at info@mvantellingen.nl for more information. If you just have a - random question and don't intent to actually pay me for my support then please - DO NOT email me at that e-mail address but just use stackoverflow or something.. - - .. _let me know: https://github.com/mvantellingen/python-zeep/issues + contact me at info@mvantellingen.nl for more information. Note that asking + questions or reporting bugs via this e-mail address will be ignored. Pleae use + the appropriate channels for that (e.g. stackoverflow) Platform: UNKNOWN -Classifier: Development Status :: 4 - Beta +Classifier: Development Status :: 5 - Production/Stable Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 diff --git a/src/zeep.egg-info/SOURCES.txt b/src/zeep.egg-info/SOURCES.txt index af3d3dd..ab2e621 100644 --- a/src/zeep.egg-info/SOURCES.txt +++ b/src/zeep.egg-info/SOURCES.txt @@ -1,5 +1,7 @@ CHANGES +CONTRIBUTORS.rst LICENSE +MANIFEST.in README.rst setup.cfg setup.py @@ -16,8 +18,8 @@ src/zeep/cache.py src/zeep/client.py src/zeep/exceptions.py src/zeep/helpers.py +src/zeep/loader.py src/zeep/ns.py -src/zeep/parser.py src/zeep/plugins.py src/zeep/transports.py src/zeep/utils.py @@ -45,6 +47,7 @@ src/zeep/wsdl/messages/__init__.py src/zeep/wsdl/messages/base.py src/zeep/wsdl/messages/http.py src/zeep/wsdl/messages/mime.py +src/zeep/wsdl/messages/multiref.py src/zeep/wsdl/messages/soap.py src/zeep/wsse/__init__.py src/zeep/wsse/compose.py @@ -74,6 +77,7 @@ src/zeep/xsd/types/builtins.py src/zeep/xsd/types/collection.py src/zeep/xsd/types/complex.py src/zeep/xsd/types/simple.py +src/zeep/xsd/types/unresolved.py tests/__init__.py tests/cert_valid.pem tests/cert_valid_pw.pem @@ -83,9 +87,11 @@ tests/test_cache.py tests/test_client.py tests/test_client_factory.py tests/test_helpers.py +tests/test_loader.py tests/test_main.py tests/test_pprint.py tests/test_response.py +tests/test_soap_multiref.py tests/test_transports.py tests/test_wsa.py tests/test_wsdl.py @@ -101,9 +107,12 @@ tests/test_xsd.py tests/test_xsd_any.py tests/test_xsd_attributes.py tests/test_xsd_builtins.py -tests/test_xsd_choice.py tests/test_xsd_complex_types.py tests/test_xsd_extension.py +tests/test_xsd_indicators_all.py +tests/test_xsd_indicators_choice.py +tests/test_xsd_indicators_group.py +tests/test_xsd_indicators_sequence.py tests/test_xsd_integration.py tests/test_xsd_parse.py tests/test_xsd_schemas.py diff --git a/src/zeep.egg-info/requires.txt b/src/zeep.egg-info/requires.txt index fe4a79f..bbcba39 100644 --- a/src/zeep.egg-info/requires.txt +++ b/src/zeep.egg-info/requires.txt @@ -1,10 +1,10 @@ appdirs>=1.4.0 -cached-property>=1.0.0 +cached-property>=1.3.0 defusedxml>=0.4.1 isodate>=0.5.4 lxml>=3.0.0 requests>=2.7.0 -requests-toolbelt>=0.7.0 +requests-toolbelt>=0.7.1 six>=1.9.0 pytz @@ -25,6 +25,7 @@ isort==4.2.5 flake8==3.2.1 flake8-blind-except==0.1.1 flake8-debugger==1.4.0 +aioresponses>=0.1.3 [xmlsec] xmlsec>=0.6.1 diff --git a/src/zeep/__init__.py b/src/zeep/__init__.py index 0cb7e04..faa5a67 100644 --- a/src/zeep/__init__.py +++ b/src/zeep/__init__.py @@ -1,6 +1,6 @@ -from zeep.client import Client # noqa +from zeep.client import CachingClient, Client # noqa from zeep.transports import Transport # noqa from zeep.plugins import Plugin # noqa from zeep.xsd.valueobjects import AnyObject # noqa -__version__ = '0.27.0' +__version__ = '2.1.1' diff --git a/src/zeep/__main__.py b/src/zeep/__main__.py index ff3d509..72afadd 100644 --- a/src/zeep/__main__.py +++ b/src/zeep/__main__.py @@ -7,6 +7,7 @@ import time import requests from six.moves.urllib.parse import urlparse + from zeep.cache import SqliteCache from zeep.client import Client from zeep.transports import Transport @@ -27,6 +28,9 @@ def parse_arguments(args=None): '--verbose', action='store_true', help='Enable verbose output') parser.add_argument( '--profile', help="Enable profiling and save output to given file") + parser.add_argument( + '--no-strict', action='store_true', default=False, + help="Disable strict mode") return parser.parse_args(args) @@ -72,7 +76,9 @@ def main(args): transport = Transport(cache=cache, session=session) st = time.time() - client = Client(args.wsdl_file, transport=transport) + + strict = not args.no_strict + client = Client(args.wsdl_file, transport=transport, strict=strict) logger.debug("Loading WSDL took %sms", (time.time() - st) * 1000) if args.profile: diff --git a/src/zeep/asyncio/transport.py b/src/zeep/asyncio/transport.py index 6968c2a..ec3cc40 100644 --- a/src/zeep/asyncio/transport.py +++ b/src/zeep/asyncio/transport.py @@ -6,6 +6,8 @@ import asyncio import logging import aiohttp +from requests import Response + from zeep.transports import Transport from zeep.utils import get_version from zeep.wsdl.utils import etree_to_string @@ -27,9 +29,14 @@ class AsyncTransport(Transport): self.logger = logging.getLogger(__name__) self.session = session or aiohttp.ClientSession(loop=self.loop) + self._close_session = session is None self.session._default_headers['User-Agent'] = ( 'Zeep/%s (www.python-zeep.org)' % (get_version())) + def __del__(self): + if self._close_session: + self.session.close() + def _load_remote_data(self, url): result = None @@ -56,20 +63,21 @@ class AsyncTransport(Transport): async def post_xml(self, address, envelope, headers): message = etree_to_string(envelope) response = await self.post(address, message, headers) - - from pretend import stub - return stub( - content=await response.read(), - status_code=response.status, - headers=response.headers) + return await self.new_response(response) async def get(self, address, params, headers): with aiohttp.Timeout(self.operation_timeout): response = await self.session.get( address, params=params, headers=headers) - from pretend import stub - return await stub( - content=await response.read(), - status_code=response.status, - headers=response.headers) + return await self.new_response(response) + + async def new_response(self, response): + """Convert an aiohttp.Response object to a requests.Response object""" + new = Response() + new._content = await response.read() + new.status_code = response.status + new.headers = response.headers + new.cookies = response.cookies + new.encoding = response.charset + return new diff --git a/src/zeep/client.py b/src/zeep/client.py index 9c7f30a..cad16ac 100644 --- a/src/zeep/client.py +++ b/src/zeep/client.py @@ -4,7 +4,7 @@ from contextlib import contextmanager from zeep.transports import Transport from zeep.wsdl import Document - +from zeep.xsd.const import NotSet logger = logging.getLogger(__name__) @@ -15,6 +15,12 @@ class OperationProxy(object): self._op_name = operation_name def __call__(self, *args, **kwargs): + """Call the operation with the given args and kwargs. + + :rtype: zeep.xsd.CompoundValue + + """ + if self._proxy._client._default_soapheaders: op_soapheaders = kwargs.get('_soapheaders') if op_soapheaders: @@ -42,9 +48,19 @@ class ServiceProxy(object): self._binding = binding def __getattr__(self, key): + """Return the OperationProxy for the given key. + + :rtype: OperationProxy() + + """ return self[key] def __getitem__(self, key): + """Return the OperationProxy for the given key. + + :rtype: OperationProxy() + + """ try: self._binding.get(key) except ValueError: @@ -62,9 +78,19 @@ class Factory(object): self._ns = types.get_ns_prefix(namespace) def __getattr__(self, key): + """Return the complexType or simpleType for the given localname. + + :rtype: zeep.xsd.ComplexType or zeep.xsd.AnySimpleType + + """ return self[key] def __getitem__(self, key): + """Return the complexType or simpleType for the given localname. + + :rtype: zeep.xsd.ComplexType or zeep.xsd.AnySimpleType + + """ return self._method('{%s}%s' % (self._ns, key)) @@ -81,19 +107,26 @@ class Client(object): first port defined in the service element in the WSDL document. :param plugins: a list of Plugin instances + :param xml_huge_tree: disable lxml/libxml2 security restrictions and + support very deep trees and very long text content """ def __init__(self, wsdl, wsse=None, transport=None, - service_name=None, port_name=None, plugins=None): + service_name=None, port_name=None, plugins=None, + strict=True, xml_huge_tree=False): if not wsdl: raise ValueError("No URL given for the wsdl") - self.transport = transport or Transport() - self.wsdl = Document(wsdl, self.transport) + self.transport = transport if transport is not None else Transport() + self.wsdl = Document(wsdl, self.transport, strict=strict) self.wsse = wsse self.plugins = plugins if plugins is not None else [] + self.xml_huge_tree = xml_huge_tree + + # options + self.raw_response = False self._default_service = None self._default_service_name = service_name @@ -102,7 +135,11 @@ class Client(object): @property def service(self): - """The default ServiceProxy instance""" + """The default ServiceProxy instance + + :rtype: ServiceProxy + + """ if self._default_service: return self._default_service @@ -116,7 +153,7 @@ class Client(object): return self._default_service @contextmanager - def options(self, timeout): + def options(self, timeout=NotSet, raw_response=NotSet): """Context manager to temporarily overrule various options. :param timeout: Set the timeout for POST/GET operations (not used for @@ -130,8 +167,22 @@ class Client(object): """ - with self.transport._options(timeout=timeout): - yield + # Store current options + old_raw_raw_response = self.raw_response + + # Set new options + self.raw_response = raw_response + + if timeout is not NotSet: + timeout_ctx = self.transport._options(timeout=timeout) + timeout_ctx.__enter__() + + yield + + self.raw_response = old_raw_raw_response + + if timeout is not NotSet: + timeout_ctx.__exit__(None, None, None) def bind(self, service_name=None, port_name=None): """Create a new ServiceProxy for the given service_name and port_name. @@ -163,15 +214,14 @@ class Client(object): "are: %s" % (', '.join(self.wsdl.bindings.keys()))) return ServiceProxy(self, binding, address=address) - def create_message(self, operation, service_name=None, port_name=None, - args=None, kwargs=None): - """Create the payload for the given operation.""" - service = self._get_service(service_name) - port = self._get_port(service, port_name) + def create_message(self, service, operation_name, *args, **kwargs): + """Create the payload for the given operation. - args = args or tuple() - kwargs = kwargs or {} - envelope, http_headers = port.binding._create(operation, args, kwargs) + :rtype: lxml.etree._Element + + """ + envelope, http_headers = service._binding._create( + operation_name, args, kwargs, client=self) return envelope def type_factory(self, namespace): @@ -182,15 +232,25 @@ class Client(object): factory = client.type_factory('ns0') user = factory.User(name='John') + :rtype: Factory + """ return Factory(self.wsdl.types, 'type', namespace) def get_type(self, name): - """Return the type for the given qualified name.""" + """Return the type for the given qualified name. + + :rtype: zeep.xsd.ComplexType or zeep.xsd.AnySimpleType + + """ return self.wsdl.types.get_type(name) def get_element(self, name): - """Return the element for the given qualified name.""" + """Return the element for the given qualified name. + + :rtype: zeep.xsd.Element + + """ return self.wsdl.types.get_element(name) def set_ns_prefix(self, prefix, namespace): @@ -227,3 +287,20 @@ class Client(object): else: service = next(iter(self.wsdl.services.values()), None) return service + + +class CachingClient(Client): + """Shortcut to create a caching client, for the lazy people. + + This enables the SqliteCache by default in the transport as was the default + in earlier versions of zeep. + + """ + def __init__(self, *args, **kwargs): + + # Don't use setdefault since we want to lazily init the Transport cls + from zeep.cache import SqliteCache + kwargs['transport'] = ( + kwargs.get('transport') or Transport(cache=SqliteCache())) + + super(CachingClient, self).__init__(*args, **kwargs) diff --git a/src/zeep/exceptions.py b/src/zeep/exceptions.py index 0056057..a7a4c1d 100644 --- a/src/zeep/exceptions.py +++ b/src/zeep/exceptions.py @@ -39,7 +39,11 @@ class TransportError(Error): class LookupError(Error): - pass + def __init__(self, *args, **kwargs): + self.qname = kwargs.pop('qname', None) + self.item_name = kwargs.pop('item_name', None) + self.location = kwargs.pop('location', None) + super(LookupError, self).__init__(*args, **kwargs) class NamespaceError(Error): @@ -74,3 +78,11 @@ class ValidationError(Error): class SignatureVerificationFailed(Error): pass + + +class IncompleteMessage(Error): + pass + + +class IncompleteOperation(Error): + pass diff --git a/src/zeep/loader.py b/src/zeep/loader.py new file mode 100644 index 0000000..58ea81e --- /dev/null +++ b/src/zeep/loader.py @@ -0,0 +1,113 @@ +import os.path + +from defusedxml.lxml import fromstring +from lxml import etree +from six.moves.urllib.parse import urljoin, urlparse + +from zeep.exceptions import XMLSyntaxError + + +class ImportResolver(etree.Resolver): + """Custom lxml resolve to use the transport object""" + def __init__(self, transport): + self.transport = transport + + def resolve(self, url, pubid, context): + if urlparse(url).scheme in ('http', 'https'): + content = self.transport.load(url) + return self.resolve_string(content, context) + + +def parse_xml(content, transport, base_url=None, strict=True, + xml_huge_tree=False): + """Parse an XML string and return the root Element. + + :param content: The XML string + :type content: str + :param transport: The transport instance to load imported documents + :type transport: zeep.transports.Transport + :param base_url: The base url of the document, used to make relative + lookups absolute. + :type base_url: str + :param strict: boolean to indicate if the lxml should be parsed a 'strict'. + If false then the recover mode is enabled which tries to parse invalid + XML as best as it can. + :param xml_huge_tree: boolean to indicate if lxml should process very + large XML content. + :type strict: boolean + :returns: The document root + :rtype: lxml.etree._Element + + """ + recover = not strict + parser = etree.XMLParser(remove_comments=True, resolve_entities=False, + recover=recover, huge_tree=xml_huge_tree) + parser.resolvers.add(ImportResolver(transport)) + try: + return fromstring(content, parser=parser, base_url=base_url) + except etree.XMLSyntaxError as exc: + raise XMLSyntaxError("Invalid XML content received (%s)" % exc.msg) + + +def load_external(url, transport, base_url=None, strict=True): + """Load an external XML document. + + :param url: + :param transport: + :param base_url: + :param strict: boolean to indicate if the lxml should be parsed a 'strict'. + If false then the recover mode is enabled which tries to parse invalid + XML as best as it can. + :type strict: boolean + + """ + if hasattr(url, 'read'): + content = url.read() + else: + if base_url: + url = absolute_location(url, base_url) + content = transport.load(url) + return parse_xml(content, transport, base_url, strict=strict) + + +def absolute_location(location, base): + """Make an url absolute (if it is optional) via the passed base url. + + :param location: The (relative) url + :type location: str + :param base: The base location + :type base: str + :returns: An absolute URL + :rtype: str + + """ + if location == base: + return location + + if urlparse(location).scheme in ('http', 'https', 'file'): + return location + + if base and urlparse(base).scheme in ('http', 'https', 'file'): + return urljoin(base, location) + else: + if os.path.isabs(location): + return location + if base: + return os.path.realpath( + os.path.join(os.path.dirname(base), location)) + return location + + +def is_relative_path(value): + """Check if the given value is a relative path + + :param value: The value + :type value: str + :returns: Boolean indicating if the url is relative. If it is absolute then + False is returned. + :rtype: boolean + + """ + if urlparse(value).scheme in ('http', 'https', 'file'): + return False + return not os.path.isabs(value) diff --git a/src/zeep/ns.py b/src/zeep/ns.py index 66e1470..b78ebdf 100644 --- a/src/zeep/ns.py +++ b/src/zeep/ns.py @@ -4,6 +4,7 @@ SOAP_12 = 'http://schemas.xmlsoap.org/wsdl/soap12/' SOAP_ENV_11 = 'http://schemas.xmlsoap.org/soap/envelope/' SOAP_ENV_12 = 'http://www.w3.org/2003/05/soap-envelope' +XSI = 'http://www.w3.org/2001/XMLSchema-instance' XSD = 'http://www.w3.org/2001/XMLSchema' WSDL = 'http://schemas.xmlsoap.org/wsdl/' @@ -16,3 +17,7 @@ WSA = 'http://www.w3.org/2005/08/addressing' DS = 'http://www.w3.org/2000/09/xmldsig#' WSSE = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd' WSU = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd' + +NAMESPACE_TO_PREFIX = { + XSD: 'xsd', +} diff --git a/src/zeep/parser.py b/src/zeep/parser.py deleted file mode 100644 index 4d6082f..0000000 --- a/src/zeep/parser.py +++ /dev/null @@ -1,49 +0,0 @@ -import os - -from defusedxml.lxml import fromstring -from lxml import etree - -from six.moves.urllib.parse import urljoin, urlparse -from zeep.exceptions import XMLSyntaxError - - -def parse_xml(content, base_url=None, recover=False): - parser = etree.XMLParser( - remove_comments=True, recover=recover, resolve_entities=False) - try: - return fromstring(content, parser=parser, base_url=base_url) - except etree.XMLSyntaxError as exc: - raise XMLSyntaxError("Invalid XML content received (%s)" % exc) - - -def load_external(url, transport, base_url=None): - if base_url: - url = absolute_location(url, base_url) - - response = transport.load(url) - return parse_xml(response, base_url) - - -def absolute_location(location, base): - if location == base: - return location - - if urlparse(location).scheme in ('http', 'https'): - return location - - if base and urlparse(base).scheme in ('http', 'https'): - return urljoin(base, location) - else: - if os.path.isabs(location): - return location - if base: - return os.path.realpath( - os.path.join(os.path.dirname(base), location)) - return location - - -def is_relative_path(value): - """Check if the given value is a relative path""" - if urlparse(value).scheme in ('http', 'https', 'file'): - return False - return not os.path.isabs(value) diff --git a/src/zeep/transports.py b/src/zeep/transports.py index 89fd183..29a4453 100644 --- a/src/zeep/transports.py +++ b/src/zeep/transports.py @@ -3,9 +3,9 @@ import os from contextlib import contextmanager import requests - from six.moves.urllib.parse import urlparse -from zeep.utils import get_version + +from zeep.utils import get_media_type, get_version from zeep.wsdl.utils import etree_to_string @@ -16,7 +16,7 @@ class Transport(object): :param timeout: The timeout for loading wsdl and xsd documents. :param operation_timeout: The timeout for operations (POST/GET). By default this is None (no timeout). - :param session: A request.Session() object (optional) + :param session: A :py:class:`request.Session()` object (optional) """ supports_async = False @@ -68,7 +68,10 @@ class Transport(object): timeout=self.operation_timeout) if self.logger.isEnabledFor(logging.DEBUG): - if 'multipart/related' in response.headers.get('Content-Type'): + media_type = get_media_type( + response.headers.get('Content-Type', 'text/xml')) + + if media_type == 'multipart/related': log_message = response.content else: log_message = response.content diff --git a/src/zeep/utils.py b/src/zeep/utils.py index d7efe90..2d15c51 100644 --- a/src/zeep/utils.py +++ b/src/zeep/utils.py @@ -1,7 +1,11 @@ +import cgi import inspect from lxml import etree +from zeep.exceptions import XMLParseError +from zeep.ns import XSD + def qname_attr(node, attr_name, target_namespace=None): value = node.get(attr_name) @@ -9,11 +13,25 @@ def qname_attr(node, attr_name, target_namespace=None): return as_qname(value, node.nsmap, target_namespace) -def as_qname(value, nsmap, target_namespace): +def as_qname(value, nsmap, target_namespace=None): """Convert the given value to a QName""" if ':' in value: prefix, local = value.split(':') - namespace = nsmap.get(prefix, prefix) + + # The xml: prefix is always bound to the XML namespace, see + # https://www.w3.org/TR/xml-names/ + if prefix == 'xml': + namespace = 'http://www.w3.org/XML/1998/namespace' + else: + namespace = nsmap.get(prefix) + + if not namespace: + raise XMLParseError("No namespace defined for %r" % prefix) + + # Workaround for https://github.com/mvantellingen/python-zeep/issues/349 + if not local: + return etree.QName(XSD, 'anyType') + return etree.QName(namespace, local) if target_namespace: @@ -61,3 +79,9 @@ def get_base_class(objects): def detect_soap_env(envelope): root_tag = etree.QName(envelope) return root_tag.namespace + + +def get_media_type(value): + """Parse a HTTP content-type header and return the media-type""" + main_value, parameters = cgi.parse_header(value) + return main_value diff --git a/src/zeep/wsa.py b/src/zeep/wsa.py index 3c3e653..a4ff68e 100644 --- a/src/zeep/wsa.py +++ b/src/zeep/wsa.py @@ -37,7 +37,5 @@ class WsAddressingPlugin(Plugin): keep_ns_prefixes=header.nsmap, top_nsmap=self.nsmap) else: - etree.cleanup_namespaces( - header, - keep_ns_prefixes=header.nsmap) + etree.cleanup_namespaces(header) return envelope, http_headers diff --git a/src/zeep/wsdl/__init__.py b/src/zeep/wsdl/__init__.py index 2dbac73..f24188e 100644 --- a/src/zeep/wsdl/__init__.py +++ b/src/zeep/wsdl/__init__.py @@ -1 +1,16 @@ +""" + zeep.wsdl + --------- + + The wsdl module is responsible for parsing the WSDL document. This includes + the bindings and messages. + + The structure and naming of the modules and classses closely follows the + WSDL 1.1 specification. + + The serialization and deserialization of the SOAP/HTTP messages is done + by the zeep.wsdl.messages modules. + + +""" from zeep.wsdl.wsdl import Document # noqa diff --git a/src/zeep/wsdl/attachments.py b/src/zeep/wsdl/attachments.py index 408ae4d..505980c 100644 --- a/src/zeep/wsdl/attachments.py +++ b/src/zeep/wsdl/attachments.py @@ -3,8 +3,8 @@ See https://www.w3.org/TR/SOAP-attachments """ + import base64 -from io import BytesIO from cached_property import cached_property from requests.structures import CaseInsensitiveDict @@ -27,9 +27,21 @@ class MessagePack(object): @cached_property def attachments(self): + """Return a list of attachments. + + :rtype: list of Attachment + + """ return [Attachment(part) for part in self._parts] def get_by_content_id(self, content_id): + """get_by_content_id + + :param content_id: The content-id to return + :type content_id: str + :rtype: Attachment + + """ for attachment in self.attachments: if attachment.content_id == content_id: return attachment @@ -37,9 +49,9 @@ class MessagePack(object): class Attachment(object): def __init__(self, part): - + encoding = part.encoding or 'utf-8' self.headers = CaseInsensitiveDict({ - k.decode(part.encoding): v.decode(part.encoding) + k.decode(encoding): v.decode(encoding) for k, v in part.headers.items() }) self.content_type = self.headers.get('Content-Type', None) @@ -52,6 +64,11 @@ class Attachment(object): @cached_property def content(self): + """Return the content of the attachment + + :rtype: bytes or str + + """ encoding = self.headers.get('Content-Transfer-Encoding', None) content = self._part.content diff --git a/src/zeep/wsdl/bindings/soap.py b/src/zeep/wsdl/bindings/soap.py index 6ba92e1..ae08ad7 100644 --- a/src/zeep/wsdl/bindings/soap.py +++ b/src/zeep/wsdl/bindings/soap.py @@ -5,8 +5,8 @@ from requests_toolbelt.multipart.decoder import MultipartDecoder from zeep import ns, plugins, wsa from zeep.exceptions import Fault, TransportError, XMLSyntaxError -from zeep.parser import parse_xml -from zeep.utils import as_qname, qname_attr +from zeep.loader import parse_xml +from zeep.utils import as_qname, get_media_type, qname_attr from zeep.wsdl.attachments import MessagePack from zeep.wsdl.definitions import Binding, Operation from zeep.wsdl.messages import DocumentMessage, RpcMessage @@ -97,9 +97,9 @@ class SoapBinding(Binding): :type options: dict :param operation: The operation object from which this is a reply :type operation: zeep.wsdl.definitions.Operation - :param args: The *args to pass to the operation + :param args: The args to pass to the operation :type args: tuple - :param kwargs: The **kwargs to pass to the operation + :param kwargs: The kwargs to pass to the operation :type kwargs: dict """ @@ -112,6 +112,11 @@ class SoapBinding(Binding): options['address'], envelope, http_headers) operation_obj = self.get(operation) + + # If the client wants to return the raw data then let's do that. + if client.raw_response: + return response + return self.process_reply(client, operation_obj, response) def process_reply(self, client, operation, response): @@ -131,20 +136,27 @@ class SoapBinding(Binding): % response.status_code) content_type = response.headers.get('Content-Type', 'text/xml') - if 'multipart/related' in content_type: - decoder = MultipartDecoder(response.content, content_type, 'utf-8') + media_type = get_media_type(content_type) + message_pack = None + + if media_type == 'multipart/related': + decoder = MultipartDecoder( + response.content, content_type, response.encoding or 'utf-8') + content = decoder.parts[0].content if len(decoder.parts) > 1: message_pack = MessagePack(parts=decoder.parts[1:]) else: content = response.content - message_pack = None try: - doc = parse_xml(content) + doc = parse_xml( + content, self.transport, + strict=client.wsdl.strict, + xml_huge_tree=client.xml_huge_tree) except XMLSyntaxError: raise TransportError( - u'Server returned HTTP status %d (%s)' + 'Server returned HTTP status %d (%s)' % (response.status_code, response.content)) if client.wsse: @@ -187,6 +199,9 @@ class SoapBinding(Binding): @classmethod def parse(cls, definitions, xmlelement): """ + + Definition:: + * <-- extensibility element (1) --> * * @@ -210,7 +225,13 @@ class SoapBinding(Binding): # default style attribute for the operations. soap_node = xmlelement.find('soap:binding', namespaces=cls.nsmap) transport = soap_node.get('transport') - if transport != 'http://schemas.xmlsoap.org/soap/http': + + supported_transports = [ + 'http://schemas.xmlsoap.org/soap/http', + 'http://www.w3.org/2003/05/soap/bindings/HTTP/', + ] + + if transport not in supported_transports: raise NotImplementedError( "The binding transport %s is not supported (only soap/http)" % ( transport)) @@ -328,12 +349,15 @@ class SoapOperation(Operation): "{%s}Envelope root element. The root element found is %s " ) % (envelope_qname.namespace, envelope.tag)) - return self.output.deserialize(envelope) + if self.output: + return self.output.deserialize(envelope) @classmethod def parse(cls, definitions, xmlelement, binding, nsmap): """ + Definition:: + * ? ? diff --git a/src/zeep/wsdl/definitions.py b/src/zeep/wsdl/definitions.py index b01e1c2..41194b8 100644 --- a/src/zeep/wsdl/definitions.py +++ b/src/zeep/wsdl/definitions.py @@ -1,7 +1,27 @@ +""" + zeep.wsdl.definitions + ~~~~~~~~~~~~~~~~~~~~~ + + A WSDL document exists out of a number of definitions. There are 6 major + definitions, these are: + + - types + - message + - portType + - binding + - port + - service + + This module defines the definitions which occur within a WSDL document, + +""" +import warnings from collections import OrderedDict, namedtuple from six import python_2_unicode_compatible +from zeep.exceptions import IncompleteOperation + MessagePart = namedtuple('MessagePart', ['element', 'type']) @@ -13,8 +33,8 @@ class AbstractMessage(object): extensible. WSDL defines several such message-typing attributes for use with XSD: - element: Refers to an XSD element using a QName. - type: Refers to an XSD simpleType or complexType using a QName. + - element: Refers to an XSD element using a QName. + - type: Refers to an XSD simpleType or complexType using a QName. """ def __init__(self, name): @@ -72,6 +92,8 @@ class PortType(object): class Binding(object): """Base class for the various bindings (SoapBinding / HttpBinding) + .. raw:: ascii + Binding | +-> Operation @@ -100,8 +122,13 @@ class Binding(object): def resolve(self, definitions): self.port_type = definitions.get('port_types', self.port_name.text) - for operation in self._operations.values(): - operation.resolve(definitions) + + for name, operation in list(self._operations.items()): + try: + operation.resolve(definitions) + except IncompleteOperation as exc: + warnings.warn(str(exc)) + del self._operations[name] def _operation_add(self, operation): # XXX: operation name is not unique @@ -146,7 +173,12 @@ class Operation(object): self.faults = {} def resolve(self, definitions): - self.abstract = self.binding.port_type.operations[self.name] + try: + self.abstract = self.binding.port_type.operations[self.name] + except KeyError: + raise IncompleteOperation( + "The wsdl:operation %r was not found in the wsdl:portType %r" % ( + self.name, self.binding.port_type.name.text)) def __repr__(self): return '<%s(name=%r, style=%r)>' % ( @@ -170,6 +202,9 @@ class Operation(object): @classmethod def parse(cls, wsdl, xmlelement, binding): """ + + Definition:: + * <-- extensibility element (2) --> * ? @@ -182,12 +217,17 @@ class Operation(object): <-- extensibility element (5) --> * + """ raise NotImplementedError() @python_2_unicode_compatible class Port(object): + """Specifies an address for a binding, thus defining a single communication + endpoint. + + """ def __init__(self, name, binding_name, xmlelement): self.name = name self._resolve_context = { @@ -231,7 +271,9 @@ class Port(object): @python_2_unicode_compatible class Service(object): + """Used to aggregate a set of related ports. + """ def __init__(self, name): self.ports = OrderedDict() self.name = name diff --git a/src/zeep/wsdl/messages/__init__.py b/src/zeep/wsdl/messages/__init__.py index 70a5b5a..f77f710 100644 --- a/src/zeep/wsdl/messages/__init__.py +++ b/src/zeep/wsdl/messages/__init__.py @@ -1,3 +1,20 @@ +""" + zeep.wsdl.messages + ~~~~~~~~~~~~~~~~~~ + + The messages are responsible for serializing and deserializing + + .. inheritance-diagram:: + zeep.wsdl.messages.soap.DocumentMessage + zeep.wsdl.messages.soap.RpcMessage + zeep.wsdl.messages.http.UrlEncoded + zeep.wsdl.messages.http.UrlReplacement + zeep.wsdl.messages.mime.MimeContent + zeep.wsdl.messages.mime.MimeXML + zeep.wsdl.messages.mime.MimeMultipart + :parts: 1 + +""" from .http import * # noqa from .mime import * # noqa from .soap import * # noqa diff --git a/src/zeep/wsdl/messages/base.py b/src/zeep/wsdl/messages/base.py index cc71e93..eebb840 100644 --- a/src/zeep/wsdl/messages/base.py +++ b/src/zeep/wsdl/messages/base.py @@ -1,3 +1,8 @@ +""" + zeep.wsdl.messages.base + ~~~~~~~~~~~~~~~~~~~~~~~ + +""" from collections import namedtuple from zeep import xsd @@ -31,15 +36,17 @@ class ConcreteMessage(object): if isinstance(self.body.type, xsd.ComplexType): try: if len(self.body.type.elements) == 1: - return self.body.type.elements[0][1].type.signature() + return self.body.type.elements[0][1].type.signature( + schema=self.wsdl.types, standalone=False) except AttributeError: return None - return self.body.type.signature() + return self.body.type.signature(schema=self.wsdl.types, standalone=False) - parts = [self.body.type.signature()] + parts = [self.body.type.signature(schema=self.wsdl.types, standalone=False)] if getattr(self, 'header', None): - parts.append('_soapheaders={%s}' % self.header.signature()) + parts.append('_soapheaders={%s}' % self.header.signature( + schema=self.wsdl.types), standalone=False) return ', '.join(part for part in parts if part) @classmethod diff --git a/src/zeep/wsdl/messages/http.py b/src/zeep/wsdl/messages/http.py index 6264dca..30ac121 100644 --- a/src/zeep/wsdl/messages/http.py +++ b/src/zeep/wsdl/messages/http.py @@ -1,3 +1,8 @@ +""" + zeep.wsdl.messages.http + ~~~~~~~~~~~~~~~~~~~~~~~ + +""" from zeep import xsd from zeep.wsdl.messages.base import ConcreteMessage, SerializedMessage diff --git a/src/zeep/wsdl/messages/mime.py b/src/zeep/wsdl/messages/mime.py index d509212..acc00a7 100644 --- a/src/zeep/wsdl/messages/mime.py +++ b/src/zeep/wsdl/messages/mime.py @@ -1,3 +1,8 @@ +""" + zeep.wsdl.messages.mime + ~~~~~~~~~~~~~~~~~~~~~~~ + +""" import six from defusedxml.lxml import fromstring from lxml import etree @@ -79,6 +84,14 @@ class MimeContent(MimeMessage): The set of defined MIME types is both large and evolving, so it is not a goal for WSDL to exhaustively define XML grammar for each MIME type. + :param wsdl: The main wsdl document + :type wsdl: zeep.wsdl.wsdl.Document + :param name: + :param operation: The operation to which this message belongs + :type operation: zeep.wsdl.bindings.soap.SoapOperation + :param part_name: + :type type: str + """ def __init__(self, wsdl, name, operation, content_type, part_name): super(MimeContent, self).__init__(wsdl, name, operation, part_name) @@ -131,6 +144,14 @@ class MimeXML(MimeMessage): only a single part. The part references a concrete schema using the element attribute for simple parts or type attribute for composite parts + :param wsdl: The main wsdl document + :type wsdl: zeep.wsdl.wsdl.Document + :param name: + :param operation: The operation to which this message belongs + :type operation: zeep.wsdl.bindings.soap.SoapOperation + :param part_name: + :type type: str + """ def serialize(self, *args, **kwargs): raise NotImplementedError() @@ -170,5 +191,13 @@ class MimeMultipart(MimeMessage): the part. If more than one MIME element appears inside a mime:part, they are alternatives. + :param wsdl: The main wsdl document + :type wsdl: zeep.wsdl.wsdl.Document + :param name: + :param operation: The operation to which this message belongs + :type operation: zeep.wsdl.bindings.soap.SoapOperation + :param part_name: + :type type: str + """ pass diff --git a/src/zeep/wsdl/messages/multiref.py b/src/zeep/wsdl/messages/multiref.py new file mode 100644 index 0000000..907049e --- /dev/null +++ b/src/zeep/wsdl/messages/multiref.py @@ -0,0 +1,81 @@ +import copy + +from lxml import etree + + +def process_multiref(node): + """Iterate through the tree and replace the referened elements. + + This method replaces the nodes with an href attribute and replaces it + with the elements it's referencing to (which have an id attribute).abs + + """ + multiref_objects = { + elm.attrib['id']: elm for elm in node.xpath('*[@id]') + } + if not multiref_objects: + return + + used_nodes = [] + + def process(node): + # TODO (In Soap 1.2 this is 'ref') + href = node.attrib.get('href') + + if href and href.startswith('#'): + obj = multiref_objects.get(href[1:]) + if obj is not None: + used_nodes.append(obj) + parent = node.getparent() + + new = _dereference_element(obj, node) + + # Replace the node with the new dereferenced node + parent.insert(parent.index(node), new) + parent.remove(node) + node = new + + for child in node: + process(child) + + process(node) + + # Remove the old dereferenced nodes from the tree + for node in used_nodes: + parent = node.getparent() + if parent is not None: + parent.remove(node) + + +def _dereference_element(source, target): + reverse_nsmap = {v: k for k, v in target.nsmap.items()} + specific_nsmap = {k: v for k, v in source.nsmap.items() if k not in target.nsmap} + + new = etree.Element(target.tag, nsmap=specific_nsmap) + + # Copy the attributes. This is actually the difficult part since the + # namespace prefixes can change in the attribute values. So for example + # the xsi:type="ns11:my-type" need's to be parsed to use a new global + # prefix. + for key, value in source.attrib.items(): + if key == 'id': + continue + + setted = False + if value.count(':') == 1: + prefix, localname = value.split(':') + if prefix in specific_nsmap: + namespace = specific_nsmap[prefix] + if namespace in reverse_nsmap: + new.set(key, '%s:%s' % (reverse_nsmap[namespace], localname)) + setted = True + + if not setted: + new.set(key, value) + + # Copy the children and the text content + for child in source: + new.append(copy.deepcopy(child)) + new.text = source.text + + return new diff --git a/src/zeep/wsdl/messages/soap.py b/src/zeep/wsdl/messages/soap.py index 17a2d2d..940a2e6 100644 --- a/src/zeep/wsdl/messages/soap.py +++ b/src/zeep/wsdl/messages/soap.py @@ -1,12 +1,19 @@ +""" + zeep.wsdl.messages.soap + ~~~~~~~~~~~~~~~~~~~~~~~ + +""" import copy from collections import OrderedDict from lxml import etree from lxml.builder import ElementMaker +from zeep import ns from zeep import exceptions, xsd from zeep.utils import as_qname from zeep.wsdl.messages.base import ConcreteMessage, SerializedMessage +from zeep.wsdl.messages.multiref import process_multiref __all__ = [ 'DocumentMessage', @@ -15,8 +22,19 @@ __all__ = [ class SoapMessage(ConcreteMessage): - """Base class for the SOAP Document and RPC messages""" + """Base class for the SOAP Document and RPC messages + :param wsdl: The main wsdl document + :type wsdl: zeep.wsdl.Document + :param name: + :param operation: The operation to which this message belongs + :type operation: zeep.wsdl.bindings.soap.SoapOperation + :param type: 'input' or 'output' + :type type: str + :param nsmap: The namespace mapping + :type nsmap: dict + + """ def __init__(self, wsdl, name, operation, type, nsmap): super(SoapMessage, self).__init__(wsdl, name, operation) self.nsmap = nsmap @@ -71,6 +89,7 @@ class SoapMessage(ConcreteMessage): if not self.envelope: return None + body = envelope.find('soap-env:Body', namespaces=self.nsmap) body_result = self._deserialize_body(body) @@ -96,7 +115,8 @@ class SoapMessage(ConcreteMessage): result = next(iter(result.__values__.values())) if isinstance(result, xsd.CompoundValue): children = result._xsd_type.elements - if len(children) == 1: + attributes = result._xsd_type.attributes + if len(children) == 1 and len(attributes) == 0: item_name, item_element = children[0] retval = getattr(result, item_name) return retval @@ -110,14 +130,16 @@ class SoapMessage(ConcreteMessage): if isinstance(self.envelope.type, xsd.ComplexType): try: if len(self.envelope.type.elements) == 1: - return self.envelope.type.elements[0][1].type.signature() + return self.envelope.type.elements[0][1].type.signature( + schema=self.wsdl.types, standalone=False) except AttributeError: return None - return self.envelope.type.signature() + return self.envelope.type.signature(schema=self.wsdl.types, standalone=False) - parts = [self.body.type.signature()] + parts = [self.body.type.signature(schema=self.wsdl.types, standalone=False)] if self.header.type._element: - parts.append('_soapheaders={%s}' % self.header.signature()) + parts.append('_soapheaders={%s}' % self.header.type.signature( + schema=self.wsdl.types, standalone=False)) return ', '.join(part for part in parts if part) @classmethod @@ -156,6 +178,8 @@ class SoapMessage(ConcreteMessage): body_data = None header_data = None + # After some profiling it turns out that .find() and .findall() in this + # case are twice as fast as the xpath method body = xmlelement.find('soap:body', namespaces=operation.binding.nsmap) if body is not None: body_data = cls._parse_body(body) @@ -270,15 +294,16 @@ class SoapMessage(ConcreteMessage): elements from the body and the headers. """ - all_elements = xsd.Sequence([ - xsd.Element('body', self.body.type), - ]) + all_elements = xsd.Sequence([]) if self.header.type._element: all_elements.append( - xsd.Element('header', self.header.type)) + xsd.Element('{%s}header' % self.nsmap['soap-env'], self.header.type)) - return xsd.Element('envelope', xsd.ComplexType(all_elements)) + all_elements.append( + xsd.Element('{%s}body' % self.nsmap['soap-env'], self.body.type)) + + return xsd.Element('{%s}envelope' % self.nsmap['soap-env'], xsd.ComplexType(all_elements)) def _serialize_header(self, headers_value, nsmap): if not headers_value: @@ -327,9 +352,9 @@ class SoapMessage(ConcreteMessage): def _resolve_header(self, info, definitions, parts): name = etree.QName(self.nsmap['soap-env'], 'Header') - sequence = xsd.Sequence() + container = xsd.All(consume_other=True) if not info: - return xsd.Element(name, xsd.ComplexType(sequence)) + return xsd.Element(name, xsd.ComplexType(container)) for item in info: message_name = item['message'].text @@ -345,14 +370,28 @@ class SoapMessage(ConcreteMessage): element.attr_name = part_name else: element = xsd.Element(part_name, part.type) - sequence.append(element) - return xsd.Element(name, xsd.ComplexType(sequence)) + container.append(element) + return xsd.Element(name, xsd.ComplexType(container)) class DocumentMessage(SoapMessage): """In the document message there are no additional wrappers, and the message parts appear directly under the SOAP Body element. + .. inheritance-diagram:: zeep.wsdl.messages.soap.DocumentMessage + :parts: 1 + + :param wsdl: The main wsdl document + :type wsdl: zeep.wsdl.Document + :param name: + :param operation: The operation to which this message belongs + :type operation: zeep.wsdl.bindings.soap.SoapOperation + :param type: 'input' or 'output' + :type type: str + :param nsmap: The namespace mapping + :type nsmap: dict + + """ def __init__(self, *args, **kwargs): @@ -407,6 +446,20 @@ class RpcMessage(SoapMessage): identically to the corresponding parameter of the call. Parts are arranged in the same order as the parameters of the call. + .. inheritance-diagram:: zeep.wsdl.messages.soap.DocumentMessage + :parts: 1 + + + :param wsdl: The main wsdl document + :type wsdl: zeep.wsdl.Document + :param name: + :param operation: The operation to which this message belongs + :type operation: zeep.wsdl.bindings.soap.SoapOperation + :param type: 'input' or 'output' + :type type: str + :param nsmap: The namespace mapping + :type nsmap: dict + """ def _resolve_body(self, info, definitions, parts): @@ -444,6 +497,8 @@ class RpcMessage(SoapMessage): element. """ + process_multiref(body_element) + response_element = body_element.getchildren()[0] if self.body: result = self.body.parse(response_element, self.wsdl.types) diff --git a/src/zeep/wsdl/parse.py b/src/zeep/wsdl/parse.py index 865ce15..f2b1506 100644 --- a/src/zeep/wsdl/parse.py +++ b/src/zeep/wsdl/parse.py @@ -1,5 +1,11 @@ +""" + zeep.wsdl.parse + ~~~~~~~~~~~~~~~ + +""" from lxml import etree +from zeep.exceptions import IncompleteMessage, LookupError, NamespaceError from zeep.utils import qname_attr from zeep.wsdl import definitions @@ -12,11 +18,20 @@ NSMAP = { def parse_abstract_message(wsdl, xmlelement): """Create an AbstractMessage object from a xml element. + Definition:: + * * + + :param wsdl: The parent definition instance + :type wsdl: zeep.wsdl.wsdl.Definition + :param xmlelement: The XML node + :type xmlelement: lxml.etree._Element + :rtype: zeep.wsdl.definitions.AbstractMessage + """ tns = wsdl.target_namespace parts = [] @@ -26,10 +41,17 @@ def parse_abstract_message(wsdl, xmlelement): part_element = qname_attr(part, 'element', tns) part_type = qname_attr(part, 'type', tns) - if part_element is not None: - part_element = wsdl.types.get_element(part_element) - if part_type is not None: - part_type = wsdl.types.get_type(part_type) + try: + if part_element is not None: + part_element = wsdl.types.get_element(part_element) + if part_type is not None: + part_type = wsdl.types.get_type(part_type) + + except (NamespaceError, LookupError): + raise IncompleteMessage(( + "The wsdl:message for %r contains " + "invalid xsd types or elements" + ) % part_name) part = definitions.MessagePart(part_element, part_type) parts.append((part_name, part)) @@ -48,6 +70,8 @@ def parse_abstract_operation(wsdl, xmlelement): This is called from the parse_port_type function since the abstract operations are part of the port type element. + Definition:: + * ? ? @@ -61,6 +85,12 @@ def parse_abstract_operation(wsdl, xmlelement): + :param wsdl: The parent definition instance + :type wsdl: zeep.wsdl.wsdl.Definition + :param xmlelement: The XML node + :type xmlelement: lxml.etree._Element + :rtype: zeep.wsdl.definitions.AbstractOperation + """ name = xmlelement.get('name') kwargs = { @@ -75,7 +105,11 @@ def parse_abstract_operation(wsdl, xmlelement): param_msg = qname_attr( msg_node, 'message', wsdl.target_namespace) param_name = msg_node.get('name') - param_value = wsdl.get('messages', param_msg.text) + + try: + param_value = wsdl.get('messages', param_msg.text) + except IndexError: + return if tag_name == 'input': kwargs['input_message'] = param_value @@ -95,18 +129,27 @@ def parse_abstract_operation(wsdl, xmlelement): def parse_port_type(wsdl, xmlelement): """Create a PortType object from a xml element. + Definition:: + * + :param wsdl: The parent definition instance + :type wsdl: zeep.wsdl.wsdl.Definition + :param xmlelement: The XML node + :type xmlelement: lxml.etree._Element + :rtype: zeep.wsdl.definitions.PortType + """ name = qname_attr(xmlelement, 'name', wsdl.target_namespace) operations = {} for elm in xmlelement.findall('wsdl:operation', namespaces=NSMAP): operation = parse_abstract_operation(wsdl, elm) - operations[operation.name] = operation + if operation: + operations[operation.name] = operation return definitions.PortType(name, operations) @@ -116,11 +159,19 @@ def parse_port(wsdl, xmlelement): This is called via the parse_service function since ports are part of the service xml elements. + Definition:: + * ? <-- extensibility element --> + :param wsdl: The parent definition instance + :type wsdl: zeep.wsdl.wsdl.Definition + :param xmlelement: The XML node + :type xmlelement: lxml.etree._Element + :rtype: zeep.wsdl.definitions.Port + """ name = xmlelement.get('name') binding_name = qname_attr(xmlelement, 'binding', wsdl.target_namespace) @@ -130,7 +181,7 @@ def parse_port(wsdl, xmlelement): def parse_service(wsdl, xmlelement): """ - Syntax:: + Definition:: * ? @@ -150,6 +201,12 @@ def parse_service(wsdl, xmlelement): + :param wsdl: The parent definition instance + :type wsdl: zeep.wsdl.wsdl.Definition + :param xmlelement: The XML node + :type xmlelement: lxml.etree._Element + :rtype: zeep.wsdl.definitions.Service + """ name = xmlelement.get('name') ports = [] diff --git a/src/zeep/wsdl/utils.py b/src/zeep/wsdl/utils.py index 6785d56..e73ac20 100644 --- a/src/zeep/wsdl/utils.py +++ b/src/zeep/wsdl/utils.py @@ -1,3 +1,8 @@ +""" + zeep.wsdl.utils + ~~~~~~~~~~~~~~~ + +""" from lxml import etree from six.moves.urllib.parse import urlparse, urlunparse diff --git a/src/zeep/wsdl/wsdl.py b/src/zeep/wsdl/wsdl.py index 23a81cb..bc43806 100644 --- a/src/zeep/wsdl/wsdl.py +++ b/src/zeep/wsdl/wsdl.py @@ -1,15 +1,21 @@ +""" + zeep.wsdl.wsdl + ~~~~~~~~~~~~~~ + +""" from __future__ import print_function import logging import operator import os +import warnings from collections import OrderedDict import six from lxml import etree -from zeep.parser import ( - absolute_location, is_relative_path, load_external, parse_xml) +from zeep.exceptions import IncompleteMessage +from zeep.loader import absolute_location, is_relative_path, load_external from zeep.utils import findall_multiple_ns from zeep.wsdl import parse from zeep.xsd import Schema @@ -34,18 +40,23 @@ class Document(object): resolves references which were not yet available during the initial parsing phase. + + :param location: Location of this WSDL + :type location: string + :param transport: The transport object to be used + :type transport: zeep.transports.Transport + :param base: The base location of this document + :type base: str + :param strict: Indicates if strict mode is enabled + :type strict: bool + """ - def __init__(self, location, transport, base=None): + def __init__(self, location, transport, base=None, strict=True): """Initialize a WSDL document. The root definition properties are exposed as entry points. - :param location: Location of this WSDL - :type location: string - :param transport: The transport object to be used - :type transport: zeep.transports.Transport - """ if isinstance(location, six.string_types): if is_relative_path(location): @@ -55,12 +66,17 @@ class Document(object): self.location = base self.transport = transport + self.strict = strict # Dict with all definition objects within this WSDL self._definitions = {} - self.types = Schema([], transport=self.transport, location=self.location) + self.types = Schema( + node=None, + transport=self.transport, + location=self.location, + strict=self.strict) - document = self._load_content(location) + document = self._get_xml_document(location) root_definitions = Definition(self, document, self.location) root_definitions.resolve_imports() @@ -75,8 +91,6 @@ class Document(object): return '' % self.location def dump(self): - namespaces = {v: k for k, v in self.types.prefix_map.items()} - print('') print("Prefixes:") for prefix, namespace in self.types.prefix_map.items(): @@ -84,18 +98,14 @@ class Document(object): print('') print("Global elements:") - for elm_obj in sorted(self.types.elements, key=lambda k: six.text_type(k)): - value = six.text_type(elm_obj) - if hasattr(elm_obj, 'qname') and elm_obj.qname.namespace: - value = '%s:%s' % (namespaces[elm_obj.qname.namespace], value) + for elm_obj in sorted(self.types.elements, key=lambda k: k.qname): + value = elm_obj.signature(schema=self.types) print(' ' * 4, value) print('') print("Global types:") for type_obj in sorted(self.types.types, key=lambda k: k.qname or ''): - value = six.text_type(type_obj) - if getattr(type_obj, 'qname', None) and type_obj.qname.namespace: - value = '%s:%s' % (namespaces[type_obj.qname.namespace], value) + value = type_obj.signature(schema=self.types) print(' ' * 4, value) print('') @@ -118,7 +128,7 @@ class Document(object): print('%s%s' % (' ' * 12, six.text_type(operation))) print('') - def _load_content(self, location): + def _get_xml_document(self, location): """Load the XML content from the given location and return an lxml.Element object. @@ -126,9 +136,8 @@ class Document(object): :type location: string """ - if hasattr(location, 'read'): - return parse_xml(location.read()) - return load_external(location, self.transport, self.location) + return load_external( + location, self.transport, self.location, strict=self.strict) def _add_definition(self, definition): key = (definition.target_namespace, definition.location) @@ -136,9 +145,18 @@ class Document(object): class Definition(object): - """The Definition represents one wsdl:definition within a Document.""" + """The Definition represents one wsdl:definition within a Document. + + :param wsdl: The wsdl + + """ def __init__(self, wsdl, doc, location): + """fo + + :param wsdl: The wsdl + + """ logger.debug("Creating definition for %s", location) self.wsdl = wsdl self.location = location @@ -183,7 +201,16 @@ class Definition(object): try: return definition.get(name, key, _processed) except IndexError: - pass + # Try to see if there is an item which has no namespace + # but where the localname matches. This is basically for + # #356 but in the future we should also ignore mismatching + # namespaces as last fallback + fallback_key = etree.QName(key).localname + try: + return definition.get(name, fallback_key, _processed) + except IndexError: + pass + raise IndexError("No definition %r in %r found" % (key, name)) def resolve_imports(self): @@ -234,7 +261,7 @@ class Definition(object): if key in self.wsdl._definitions: self.imports[key] = self.wsdl._definitions[key] else: - document = self.wsdl._load_content(location) + document = self.wsdl._get_xml_document(location) if etree.QName(document.tag).localname == 'schema': self.types.add_documents([document], location) else: @@ -251,6 +278,8 @@ class Definition(object): If the wsdl:types doesn't container an xml schema then an empty schema is returned instead. + Definition:: + * @@ -279,6 +308,9 @@ class Definition(object): def parse_messages(self, doc): """ + + Definition:: + * * @@ -291,14 +323,20 @@ class Definition(object): """ result = {} for msg_node in doc.findall("wsdl:message", namespaces=NSMAP): - msg = parse.parse_abstract_message(self, msg_node) - result[msg.name.text] = msg - logger.debug("Adding message: %s", msg.name.text) + try: + msg = parse.parse_abstract_message(self, msg_node) + except IncompleteMessage as exc: + warnings.warn(str(exc)) + else: + result[msg.name.text] = msg + logger.debug("Adding message: %s", msg.name.text) return result def parse_ports(self, doc): """Return dict with `PortType` instances as values + Definition:: + * @@ -323,7 +361,7 @@ class Definition(object): HTTP Post. The detection of the type of bindings is done by the bindings themselves using the introspection of the xml nodes. - XML Structure:: + Definition:: * @@ -345,6 +383,9 @@ class Definition(object): :param doc: The source document :type doc: lxml.etree._Element + :returns: Dictionary with binding name as key and Binding instance as + value + :rtype: dict """ result = {} @@ -382,6 +423,9 @@ class Definition(object): def parse_service(self, doc): """ + + Definition:: + * * diff --git a/src/zeep/wsse/signature.py b/src/zeep/wsse/signature.py index 120619a..ccc8e18 100644 --- a/src/zeep/wsse/signature.py +++ b/src/zeep/wsse/signature.py @@ -11,38 +11,63 @@ module. from lxml import etree from lxml.etree import QName +from zeep import ns +from zeep.exceptions import SignatureVerificationFailed +from zeep.utils import detect_soap_env +from zeep.wsse.utils import ensure_id, get_security_header + try: import xmlsec except ImportError: xmlsec = None -from zeep import ns -from zeep.utils import detect_soap_env -from zeep.exceptions import SignatureVerificationFailed -from zeep.wsse.utils import ensure_id, get_security_header # SOAP envelope SOAP_NS = 'http://schemas.xmlsoap.org/soap/envelope/' +def _read_file(f_name): + with open(f_name, "rb") as f: + return f.read() -class Signature(object): +def _make_sign_key(key_data, cert_data, password): + key = xmlsec.Key.from_memory(key_data, + xmlsec.KeyFormat.PEM, password) + key.load_cert_from_memory(cert_data, + xmlsec.KeyFormat.PEM) + return key + +def _make_verify_key(cert_data): + key = xmlsec.Key.from_memory(cert_data, + xmlsec.KeyFormat.CERT_PEM, None) + return key + +class MemorySignature(object): """Sign given SOAP envelope with WSSE sig using given key and cert.""" - def __init__(self, key_file, certfile, password=None): + def __init__(self, key_data, cert_data, password=None): check_xmlsec_import() - self.key_file = key_file - self.certfile = certfile + self.key_data = key_data + self.cert_data = cert_data self.password = password def apply(self, envelope, headers): - sign_envelope(envelope, self.key_file, self.certfile, self.password) + key = _make_sign_key(self.key_data, self.cert_data, self.password) + _sign_envelope_with_key(envelope, key) return envelope, headers def verify(self, envelope): - verify_envelope(envelope, self.certfile) + key = _make_verify_key(self.cert_data) + _verify_envelope_with_key(envelope, key) return envelope +class Signature(MemorySignature): + """Sign given SOAP envelope with WSSE sig using given key file and cert file.""" + + def __init__(self, key_file, certfile, password=None): + super(Signature, self).__init__(_read_file(key_file), + _read_file(certfile), + password) def check_xmlsec_import(): if xmlsec is None: @@ -141,6 +166,12 @@ def sign_envelope(envelope, keyfile, certfile, password=None): """ + # Load the signing key and certificate. + key = _make_sign_key(_read_file(keyfile), _read_file(certfile), password) + return _sign_envelope_with_key(envelope, key) + +def _sign_envelope_with_key(envelope, key): + # Create the Signature node. signature = xmlsec.template.create( envelope, @@ -155,10 +186,6 @@ def sign_envelope(envelope, keyfile, certfile, password=None): xmlsec.template.x509_data_add_issuer_serial(x509_data) xmlsec.template.x509_data_add_certificate(x509_data) - # Load the signing key and certificate. - key = xmlsec.Key.from_file(keyfile, xmlsec.KeyFormat.PEM, password=password) - key.load_cert_from_file(certfile, xmlsec.KeyFormat.PEM) - # Insert the Signature node in the wsse:Security header. security = get_security_header(envelope) security.insert(0, signature) @@ -190,12 +217,19 @@ def verify_envelope(envelope, certfile): Expects a document like that found in the sample XML in the ``sign()`` docstring. - Raise SignatureValidationFailed on failure, silent on success. + Raise SignatureVerificationFailed on failure, silent on success. """ + key = _make_verify_key(_read_file(certfile)) + return _verify_envelope_with_key(envelope, key) + +def _verify_envelope_with_key(envelope, key): soap_env = detect_soap_env(envelope) header = envelope.find(QName(soap_env, 'Header')) + if not header: + raise SignatureVerificationFailed() + security = header.find(QName(ns.WSSE, 'Security')) signature = security.find(QName(ns.DS, 'Signature')) @@ -213,7 +247,6 @@ def verify_envelope(envelope, certfile): )[0] ctx.register_id(referenced, 'Id', ns.WSU) - key = xmlsec.Key.from_file(certfile, xmlsec.KeyFormat.CERT_PEM, None) ctx.key = key try: diff --git a/src/zeep/wsse/utils.py b/src/zeep/wsse/utils.py index d08cfc1..4f96a38 100644 --- a/src/zeep/wsse/utils.py +++ b/src/zeep/wsse/utils.py @@ -1,8 +1,8 @@ -from uuid import uuid4 -from lxml import etree import datetime +from uuid import uuid4 import pytz +from lxml import etree from lxml.builder import ElementMaker from zeep import ns diff --git a/src/zeep/xsd/__init__.py b/src/zeep/xsd/__init__.py index 1fc975d..dfe6358 100644 --- a/src/zeep/xsd/__init__.py +++ b/src/zeep/xsd/__init__.py @@ -1,4 +1,9 @@ -from zeep.xsd.const import SkipValue # noqa +""" + zeep.xsd + -------- + +""" +from zeep.xsd.const import Nil, SkipValue # noqa from zeep.xsd.elements import * # noqa from zeep.xsd.schema import Schema # noqa from zeep.xsd.types import * # noqa diff --git a/src/zeep/xsd/const.py b/src/zeep/xsd/const.py index 7ba420c..75b4097 100644 --- a/src/zeep/xsd/const.py +++ b/src/zeep/xsd/const.py @@ -1,15 +1,13 @@ from lxml import etree -NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance' -NS_XSD = 'http://www.w3.org/2001/XMLSchema' - +from zeep import ns def xsi_ns(localname): - return etree.QName(NS_XSI, localname) + return etree.QName(ns.XSI, localname) def xsd_ns(localname): - return etree.QName(NS_XSD, localname) + return etree.QName(ns.XSD, localname) class _StaticIdentity(object): @@ -22,3 +20,4 @@ class _StaticIdentity(object): NotSet = _StaticIdentity('NotSet') SkipValue = _StaticIdentity('SkipValue') +Nil = _StaticIdentity('Nil') diff --git a/src/zeep/xsd/elements/any.py b/src/zeep/xsd/elements/any.py index d91c506..e799191 100644 --- a/src/zeep/xsd/elements/any.py +++ b/src/zeep/xsd/elements/any.py @@ -2,9 +2,9 @@ import logging from lxml import etree -from zeep import exceptions +from zeep import exceptions, ns from zeep.utils import qname_attr -from zeep.xsd.const import xsi_ns, NotSet +from zeep.xsd.const import NotSet, xsi_ns from zeep.xsd.elements.base import Base from zeep.xsd.utils import max_occurs_iter from zeep.xsd.valueobjects import AnyObject @@ -84,7 +84,19 @@ class Any(Base): return {} def parse_xmlelements(self, xmlelements, schema, name=None, context=None): - """Consume matching xmlelements and call parse() on each of them""" + """Consume matching xmlelements and call parse() on each of them + + :param xmlelements: Dequeue of XML element objects + :type xmlelements: collections.deque of lxml.etree._Element + :param schema: The parent XML schema + :type schema: zeep.xsd.Schema + :param name: The name of the parent element + :type name: str + :param context: Optional parsing context (for inline schemas) + :type context: zeep.xsd.context.XmlParserContext + :return: dict or None + + """ result = [] for _unused in max_occurs_iter(self.max_occurs): @@ -169,9 +181,10 @@ class Any(Base): # Check if we received a proper value object. If we receive the wrong # type then return a nice error message if self.restrict: - expected_types = (etree._Element,) + self.restrict.accepted_types + expected_types = (etree._Element, dict,) + self.restrict.accepted_types else: - expected_types = (etree._Element, AnyObject) + expected_types = (etree._Element, dict,AnyObject) + if not isinstance(value, expected_types): type_names = [ '%s.%s' % (t.__module__, t.__name__) for t in expected_types @@ -188,7 +201,7 @@ class Any(Base): def resolve(self): return self - def signature(self, depth=()): + def signature(self, schema=None, standalone=True): if self.restrict: base = self.restrict.name else: @@ -201,23 +214,30 @@ class Any(Base): class AnyAttribute(Base): name = None + _ignore_attributes = [ + etree.QName(ns.XSI, 'type') + ] def __init__(self, process_contents='strict'): self.qname = None self.process_contents = process_contents def parse(self, attributes, context=None): - return attributes + result = {} + for key, value in attributes.items(): + if key not in self._ignore_attributes: + result[key] = value + return result def resolve(self): return self def render(self, parent, value, render_path=None): - if value is None: + if value in (None, NotSet): return for name, val in value.items(): parent.set(name, val) - def signature(self, depth=()): + def signature(self, schema=None, standalone=True): return '{}' diff --git a/src/zeep/xsd/elements/attribute.py b/src/zeep/xsd/elements/attribute.py index 04ec751..5ca075a 100644 --- a/src/zeep/xsd/elements/attribute.py +++ b/src/zeep/xsd/elements/attribute.py @@ -88,5 +88,5 @@ class AttributeGroup(object): self._attributes = resolved return self - def signature(self, depth=()): - return ', '.join(attr.signature() for attr in self._attributes) + def signature(self, schema=None, standalone=True): + return ', '.join(attr.signature(schema) for attr in self._attributes) diff --git a/src/zeep/xsd/elements/base.py b/src/zeep/xsd/elements/base.py index 64ff835..499ccac 100644 --- a/src/zeep/xsd/elements/base.py +++ b/src/zeep/xsd/elements/base.py @@ -25,8 +25,20 @@ class Base(object): raise NotImplementedError() def parse_xmlelements(self, xmlelements, schema, name=None, context=None): - """Consume matching xmlelements and call parse() on each of them""" + """Consume matching xmlelements and call parse() on each of them + + :param xmlelements: Dequeue of XML element objects + :type xmlelements: collections.deque of lxml.etree._Element + :param schema: The parent XML schema + :type schema: zeep.xsd.Schema + :param name: The name of the parent element + :type name: str + :param context: Optional parsing context (for inline schemas) + :type context: zeep.xsd.context.XmlParserContext + :return: dict or None + + """ raise NotImplementedError() - def signature(self, depth=()): + def signature(self, schema=None, standalone=False): return '' diff --git a/src/zeep/xsd/elements/builtins.py b/src/zeep/xsd/elements/builtins.py index 5eca4c6..eedf460 100644 --- a/src/zeep/xsd/elements/builtins.py +++ b/src/zeep/xsd/elements/builtins.py @@ -35,6 +35,6 @@ class Schema(Base): return self -default_elements = { - xsd_ns('schema'): Schema(), -} +_elements = [ + Schema +] diff --git a/src/zeep/xsd/elements/element.py b/src/zeep/xsd/elements/element.py index ebb1739..204e532 100644 --- a/src/zeep/xsd/elements/element.py +++ b/src/zeep/xsd/elements/element.py @@ -6,10 +6,10 @@ from lxml import etree from zeep import exceptions from zeep.exceptions import UnexpectedElementError from zeep.utils import qname_attr -from zeep.xsd.const import NotSet, xsi_ns +from zeep.xsd.const import Nil, NotSet, xsi_ns from zeep.xsd.context import XmlParserContext from zeep.xsd.elements.base import Base -from zeep.xsd.utils import max_occurs_iter +from zeep.xsd.utils import create_prefixed_name, max_occurs_iter logger = logging.getLogger(__name__) @@ -38,7 +38,10 @@ class Element(Base): def __str__(self): if self.type: - return '%s(%s)' % (self.name, self.type.signature()) + if self.type.is_global: + return '%s(%s)' % (self.name, self.type.qname) + else: + return '%s(%s)' % (self.name, self.type.signature()) return '%s()' % self.name def __call__(self, *args, **kwargs): @@ -57,10 +60,16 @@ class Element(Base): self.__class__ == other.__class__ and self.__dict__ == other.__dict__) + def get_prefixed_name(self, schema): + return create_prefixed_name(self.qname, schema) + @property def default_value(self): - value = [] if self.accepts_multiple else self.default - return value + if self.accepts_multiple: + return [] + if self.is_optional: + return None + return self.default def clone(self, name=None, min_occurs=1, max_occurs=1): new = copy.copy(self) @@ -81,6 +90,18 @@ class Element(Base): use that for further processing. This should only be done for subtypes of the defined type but for now we just accept everything. + This is the entrypoint for parsing an xml document. + + :param xmlelement: The XML element to parse + :type xmlelements: lxml.etree._Element + :param schema: The parent XML schema + :type schema: zeep.xsd.Schema + :param allow_none: Allow none + :type allow_none: bool + :param context: Optional parsing context (for inline schemas) + :type context: zeep.xsd.context.XmlParserContext + :return: dict or None + """ context = context or XmlParserContext() instance_type = qname_attr(xmlelement, xsi_ns('type')) @@ -89,14 +110,27 @@ class Element(Base): xsd_type = schema.get_type(instance_type, fail_silently=True) xsd_type = xsd_type or self.type return xsd_type.parse_xmlelement( - xmlelement, schema, allow_none=allow_none, context=context) + xmlelement, schema, allow_none=allow_none, context=context, + schema_type=self.type) def parse_kwargs(self, kwargs, name, available_kwargs): return self.type.parse_kwargs( kwargs, name or self.attr_name, available_kwargs) def parse_xmlelements(self, xmlelements, schema, name=None, context=None): - """Consume matching xmlelements and call parse() on each of them""" + """Consume matching xmlelements and call parse() on each of them + + :param xmlelements: Dequeue of XML element objects + :type xmlelements: collections.deque of lxml.etree._Element + :param schema: The parent XML schema + :type schema: zeep.xsd.Schema + :param name: The name of the parent element + :type name: str + :param context: Optional parsing context (for inline schemas) + :type context: zeep.xsd.context.XmlParserContext + :return: dict or None + + """ result = [] num_matches = 0 for _unused in max_occurs_iter(self.max_occurs): @@ -113,7 +147,8 @@ class Element(Base): element_tag = etree.QName(xmlelements[0].tag) if ( element_tag.namespace and self.qname.namespace and - element_tag.namespace != self.qname.namespace + element_tag.namespace != self.qname.namespace and + schema.strict ): break @@ -123,8 +158,7 @@ class Element(Base): num_matches += 1 item = self.parse( xmlelement, schema, allow_none=True, context=context) - if item is not None: - result.append(item) + result.append(item) else: # If the element passed doesn't match and the current one is # not optional then throw an error @@ -158,6 +192,12 @@ class Element(Base): def _render_value_item(self, parent, value, render_path): """Render the value on the parent lxml.Element""" + + if value is Nil: + elm = etree.SubElement(parent, self.qname) + elm.set(xsi_ns('nil'), 'true') + return + if value is None or value is NotSet: if self.is_optional: return @@ -215,11 +255,19 @@ class Element(Base): self.resolve_type() return self - def signature(self, depth=()): - if len(depth) > 0 and self.is_global: - return self.name + '()' + def signature(self, schema=None, standalone=True): + from zeep.xsd import ComplexType + if self.type.is_global or (not standalone and self.is_global): + value = self.type.get_prefixed_name(schema) + else: + value = self.type.signature(schema, standalone=False) + + if not standalone and isinstance(self.type, ComplexType): + value = '{%s}' % value + + if standalone: + value = '%s(%s)' % (self.get_prefixed_name(schema), value) - value = self.type.signature(depth) if self.accepts_multiple: return '%s[]' % value return value diff --git a/src/zeep/xsd/elements/indicators.py b/src/zeep/xsd/elements/indicators.py index 282b13a..10ccf00 100644 --- a/src/zeep/xsd/elements/indicators.py +++ b/src/zeep/xsd/elements/indicators.py @@ -1,36 +1,56 @@ +""" +zeep.xsd.elements.indicators +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Indicators are a collection of elements. There are four available, these are +All, Choice, Group and Sequence. + + Indicator -> OrderIndicator -> All + -> Choice + -> Sequence + -> Group + +""" import copy import operator from collections import OrderedDict, defaultdict, deque from cached_property import threaded_cached_property -from zeep.exceptions import UnexpectedElementError +from zeep.exceptions import UnexpectedElementError, ValidationError from zeep.xsd.const import NotSet, SkipValue from zeep.xsd.elements import Any, Element from zeep.xsd.elements.base import Base from zeep.xsd.utils import ( - NamePrefixGenerator, UniqueNameGenerator, max_occurs_iter) + NamePrefixGenerator, UniqueNameGenerator, create_prefixed_name, + max_occurs_iter) __all__ = ['All', 'Choice', 'Group', 'Sequence'] class Indicator(Base): + """Base class for the other indicators""" def __repr__(self): return '<%s(%s)>' % ( self.__class__.__name__, super(Indicator, self).__repr__()) - @threaded_cached_property + @property def default_value(self): - return OrderedDict([ + values = OrderedDict([ (name, element.default_value) for name, element in self.elements ]) + if self.accepts_multiple: + return {'_value_1': values} + return values + def clone(self, name, min_occurs=1, max_occurs=1): raise NotImplementedError() class OrderIndicator(Indicator, list): + """Base class for All, Choice and Sequence classes.""" name = None def __init__(self, elements=None, min_occurs=1, max_occurs=1): @@ -85,16 +105,30 @@ class OrderIndicator(Indicator, list): If not all required elements are available then 0 is returned. """ - num = 0 - for name, element in self.elements_nested: - if isinstance(element, Element): - if element.name in values and values[element.name] is not None: - num += 1 - else: - num += element.accept(values) - return num + if not self.accepts_multiple: + values = [values] + + results = set() + for value in values: + num = 0 + for name, element in self.elements_nested: + if isinstance(element, Element): + if element.name in value and value[element.name] is not None: + num += 1 + else: + num += element.accept(value) + results.add(num) + return max(results) def parse_args(self, args, index=0): + + # If the sequence contains an choice element then we can't convert + # the args to kwargs since Choice elements don't work with position + # arguments + for name, elm in self.elements_nested: + if isinstance(elm, Choice): + raise TypeError("Choice elements only work with keyword arguments") + result = {} for name, element in self.elements: if index >= len(args): @@ -110,11 +144,24 @@ class OrderIndicator(Indicator, list): The available_kwargs is modified in-place. Returns a dict with the result. + :param kwargs: The kwargs + :type kwargs: dict + :param name: The name as which this type is registered in the parent + :type name: str + :param available_kwargs: The kwargs keys which are still available, + modified in place + :type available_kwargs: set + :rtype: dict + """ if self.accepts_multiple: assert name - if name and name in available_kwargs: + if name: + if name not in available_kwargs: + return {} + + assert self.accepts_multiple # Make sure we have a list, lame lame item_kwargs = kwargs.get(name) @@ -123,19 +170,26 @@ class OrderIndicator(Indicator, list): result = [] for item_value in max_occurs_iter(self.max_occurs, item_kwargs): - item_kwargs = set(item_value.keys()) + try: + item_kwargs = set(item_value.keys()) + except AttributeError: + raise TypeError( + "A list of dicts is expected for unbounded Sequences") + subresult = OrderedDict() for item_name, element in self.elements: value = element.parse_kwargs(item_value, item_name, item_kwargs) if value is not None: subresult.update(value) + if item_kwargs: + raise TypeError(( + "%s() got an unexpected keyword argument %r." + ) % (self, list(item_kwargs)[0])) + result.append(subresult) - if self.accepts_multiple: - result = {name: result} - else: - result = result[0] if result else None + result = {name: result} # All items consumed if not any(filter(None, item_kwargs)): @@ -144,15 +198,13 @@ class OrderIndicator(Indicator, list): return result else: + assert not self.accepts_multiple result = OrderedDict() for elm_name, element in self.elements_nested: sub_result = element.parse_kwargs(kwargs, elm_name, available_kwargs) if sub_result: result.update(sub_result) - if name: - result = {name: result} - return result def resolve(self): @@ -167,6 +219,8 @@ class OrderIndicator(Indicator, list): else: values = value + self.validate(values, render_path) + for value in max_occurs_iter(self.max_occurs, values): for name, element in self.elements_nested: if name: @@ -186,22 +240,20 @@ class OrderIndicator(Indicator, list): if element_value is not None or not element.is_optional: element.render(parent, element_value, child_path) - def signature(self, depth=()): - """ - Use a tuple of element names as depth indicator, so that when an element is repeated, - do not try to create its signature, as it would lead to infinite recursion - """ - depth += (self.name,) + def validate(self, value, render_path): + for item in value: + if item is NotSet: + raise ValidationError("No value set", path=render_path) + + def signature(self, schema=None, standalone=True): parts = [] for name, element in self.elements_nested: - if hasattr(element, 'type') and element.type.name and element.type.name in depth: - parts.append('{}: {}'.format(name, element.type.name)) - elif name: - parts.append('%s: %s' % (name, element.signature(depth))) - elif isinstance(element, Indicator): - parts.append('%s' % (element.signature(depth))) + if isinstance(element, Indicator): + parts.append(element.signature(schema, standalone=False)) else: - parts.append('%s: %s' % (name, element.signature(depth))) + value = element.signature(schema, standalone=False) + parts.append('%s: %s' % (name, value)) + part = ', '.join(parts) if self.accepts_multiple: @@ -215,7 +267,25 @@ class All(OrderIndicator): """ + def __init__(self, elements=None, min_occurs=1, max_occurs=1, + consume_other=False): + super(All, self).__init__(elements, min_occurs, max_occurs) + self._consume_other = consume_other + def parse_xmlelements(self, xmlelements, schema, name=None, context=None): + """Consume matching xmlelements + + :param xmlelements: Dequeue of XML element objects + :type xmlelements: collections.deque of lxml.etree._Element + :param schema: The parent XML schema + :type schema: zeep.xsd.Schema + :param name: The name of the parent element + :type name: str + :param context: Optional parsing context (for inline schemas) + :type context: zeep.xsd.context.XmlParserContext + :rtype: dict or None + + """ result = OrderedDict() expected_tags = {element.qname for __, element in self.elements} consumed_tags = set() @@ -236,10 +306,18 @@ class All(OrderIndicator): result[name] = element.parse_xmlelements( sub_elements, schema, context=context) + if self._consume_other and xmlelements: + result['_raw_elements'] = list(xmlelements) + xmlelements.clear() return result class Choice(OrderIndicator): + """Permits one and only one of the elements contained in the group.""" + + def parse_args(self, args, index=0): + if args: + raise TypeError("Choice elements only work with keyword arguments") @property def is_optional(self): @@ -250,49 +328,59 @@ class Choice(OrderIndicator): return OrderedDict() def parse_xmlelements(self, xmlelements, schema, name=None, context=None): - """Return a dictionary""" + """Consume matching xmlelements + + :param xmlelements: Dequeue of XML element objects + :type xmlelements: collections.deque of lxml.etree._Element + :param schema: The parent XML schema + :type schema: zeep.xsd.Schema + :param name: The name of the parent element + :type name: str + :param context: Optional parsing context (for inline schemas) + :type context: zeep.xsd.context.XmlParserContext + :rtype: dict or None + + """ result = [] for _unused in max_occurs_iter(self.max_occurs): if not xmlelements: break - for node in list(xmlelements): + # Choose out of multiple + options = [] + for element_name, element in self.elements_nested: - # Choose out of multiple - options = [] - for element_name, element in self.elements_nested: + local_xmlelements = copy.copy(xmlelements) - local_xmlelements = copy.copy(xmlelements) + try: + sub_result = element.parse_xmlelements( + xmlelements=local_xmlelements, + schema=schema, + name=element_name, + context=context) + except UnexpectedElementError: + continue - try: - sub_result = element.parse_xmlelements( - local_xmlelements, schema, context=context) - except UnexpectedElementError: - continue + if isinstance(element, Element): + sub_result = {element_name: sub_result} - if isinstance(element, OrderIndicator): - if element.accepts_multiple: - sub_result = {element_name: sub_result} - else: - sub_result = {element_name: sub_result} + num_consumed = len(xmlelements) - len(local_xmlelements) + if num_consumed: + options.append((num_consumed, sub_result)) - num_consumed = len(xmlelements) - len(local_xmlelements) - if num_consumed: - options.append((num_consumed, sub_result)) + if not options: + xmlelements = [] + break - if not options: - xmlelements = [] - break - - # Sort on least left - options = sorted(options, key=operator.itemgetter(0), reverse=True) - if options: - result.append(options[0][1]) - for i in range(options[0][0]): - xmlelements.popleft() - else: - break + # Sort on least left + options = sorted(options, key=operator.itemgetter(0), reverse=True) + if options: + result.append(options[0][1]) + for i in range(options[0][0]): + xmlelements.popleft() + else: + break if self.accepts_multiple: result = {name: result} @@ -308,7 +396,7 @@ class Choice(OrderIndicator): This handles two distinct initialization methods: 1. Passing the choice elements directly to the kwargs (unnested) - 2. Passing the choice elements into the `name` kwarg (_alue_1) (nested). + 2. Passing the choice elements into the `name` kwarg (_value_1) (nested). This case is required when multiple choice elements are given. :param name: Name of the choice element (_value_1) @@ -320,6 +408,8 @@ class Choice(OrderIndicator): """ if name and name in available_kwargs: + assert self.accepts_multiple + values = kwargs[name] or [] available_kwargs.remove(name) result = [] @@ -327,9 +417,9 @@ class Choice(OrderIndicator): if isinstance(values, dict): values = [values] + # TODO: Use most greedy choice instead of first matching for value in values: for element in self: - # TODO: Use most greedy choice instead of first matching if isinstance(element, OrderIndicator): choice_value = value[name] if name in value else value if element.accept(choice_value): @@ -356,9 +446,9 @@ class Choice(OrderIndicator): # When choice elements are specified directly in the kwargs found = False - for i, choice in enumerate(self): + for name, choice in self.elements_nested: temp_kwargs = copy.copy(available_kwargs) - subresult = choice.parse_kwargs(kwargs, None, temp_kwargs) + subresult = choice.parse_kwargs(kwargs, name, temp_kwargs) if subresult: if not any(subresult.values()): @@ -388,12 +478,24 @@ class Choice(OrderIndicator): if not self.accepts_multiple: value = [value] + self.validate(value, render_path) + for item in value: result = self._find_element_to_render(item) if result: element, choice_value = result element.render(parent, choice_value, render_path) + def validate(self, value, render_path): + found = 0 + for item in value: + result = self._find_element_to_render(item) + if result: + found += 1 + + if not found and not self.is_optional: + raise ValidationError("Missing choice values", path=render_path) + def accept(self, values): """Return the number of values which are accepted by this choice. @@ -403,15 +505,24 @@ class Choice(OrderIndicator): nums = set() for name, element in self.elements_nested: if isinstance(element, Element): - if name in values and values[name]: - nums.add(1) + if self.accepts_multiple: + if all(name in item and item[name] for item in values): + nums.add(1) + else: + if name in values and values[name]: + nums.add(1) else: num = element.accept(values) nums.add(num) return max(nums) if nums else 0 def _find_element_to_render(self, value): - """Return a tuple (element, value) for the best matching choice""" + """Return a tuple (element, value) for the best matching choice. + + This is used to decide which choice child is best suitable for + rendering the available data. + + """ matches = [] for name, element in self.elements_nested: @@ -441,13 +552,13 @@ class Choice(OrderIndicator): matches = sorted(matches, key=operator.itemgetter(0), reverse=True) return matches[0][1:] - def signature(self, depth=()): + def signature(self, schema=None, standalone=True): parts = [] for name, element in self.elements_nested: if isinstance(element, OrderIndicator): - parts.append('{%s}' % (element.signature(depth))) + parts.append('{%s}' % (element.signature(schema, standalone=False))) else: - parts.append('{%s: %s}' % (name, element.signature(depth))) + parts.append('{%s: %s}' % (name, element.signature(schema, standalone=False))) part = '(%s)' % ' | '.join(parts) if self.accepts_multiple: return '%s[]' % (part,) @@ -455,17 +566,42 @@ class Choice(OrderIndicator): class Sequence(OrderIndicator): + """Requires the elements in the group to appear in the specified sequence + within the containing element. + """ def parse_xmlelements(self, xmlelements, schema, name=None, context=None): + """Consume matching xmlelements + + :param xmlelements: Dequeue of XML element objects + :type xmlelements: collections.deque of lxml.etree._Element + :param schema: The parent XML schema + :type schema: zeep.xsd.Schema + :param name: The name of the parent element + :type name: str + :param context: Optional parsing context (for inline schemas) + :type context: zeep.xsd.context.XmlParserContext + :rtype: dict or None + + """ result = [] + + if self.accepts_multiple: + assert name + for _unused in max_occurs_iter(self.max_occurs): if not xmlelements: break item_result = OrderedDict() for elm_name, element in self.elements: - item_subresult = element.parse_xmlelements( - xmlelements, schema, name, context=context) + try: + item_subresult = element.parse_xmlelements( + xmlelements, schema, name, context=context) + except UnexpectedElementError: + if schema.strict: + raise + item_subresult = None # Unwrap if allowed if isinstance(element, OrderIndicator): @@ -480,7 +616,6 @@ class Sequence(OrderIndicator): if not self.accepts_multiple: return result[0] if result else None - return {name: result} @@ -498,15 +633,8 @@ class Group(Indicator): self.max_occurs = max_occurs self.min_occurs = min_occurs - def clone(self, name, min_occurs=1, max_occurs=1): - return self.__class__( - name=self.qname, - child=self.child, - min_occurs=min_occurs, - max_occurs=max_occurs) - def __str__(self): - return '%s(%s)' % (self.name, self.signature()) + return self.signature() def __iter__(self, *args, **kwargs): for item in self.child: @@ -518,13 +646,28 @@ class Group(Indicator): return [('_value_1', self.child)] return self.child.elements + def clone(self, name, min_occurs=1, max_occurs=1): + return self.__class__( + name=name, + child=self.child, + min_occurs=min_occurs, + max_occurs=max_occurs) + + def accept(self, values): + """Return the number of values which are accepted by this choice. + + If not all required elements are available then 0 is returned. + + """ + return self.child.accept(values) + def parse_args(self, args, index=0): return self.child.parse_args(args, index) def parse_kwargs(self, kwargs, name, available_kwargs): if self.accepts_multiple: if name not in kwargs: - return {}, kwargs + return {} available_kwargs.remove(name) item_kwargs = kwargs[name] @@ -536,6 +679,11 @@ class Group(Indicator): subresult = self.child.parse_kwargs( sub_kwargs, sub_name, available_sub_kwargs) + if available_sub_kwargs: + raise TypeError(( + "%s() got an unexpected keyword argument %r." + ) % (self, list(available_sub_kwargs)[0])) + if subresult: result.append(subresult) if result: @@ -545,6 +693,19 @@ class Group(Indicator): return result def parse_xmlelements(self, xmlelements, schema, name=None, context=None): + """Consume matching xmlelements + + :param xmlelements: Dequeue of XML element objects + :type xmlelements: collections.deque of lxml.etree._Element + :param schema: The parent XML schema + :type schema: zeep.xsd.Schema + :param name: The name of the parent element + :type name: str + :param context: Optional parsing context (for inline schemas) + :type context: zeep.xsd.context.XmlParserContext + :rtype: dict or None + + """ result = [] for _unused in max_occurs_iter(self.max_occurs): @@ -552,16 +713,29 @@ class Group(Indicator): self.child.parse_xmlelements( xmlelements, schema, name, context=context) ) + if not xmlelements: + break if not self.accepts_multiple and result: return result[0] return {name: result} - def render(self, *args, **kwargs): - return self.child.render(*args, **kwargs) + def render(self, parent, value, render_path): + if not isinstance(value, list): + values = [value] + else: + values = value + + for value in values: + self.child.render(parent, value, render_path) def resolve(self): self.child = self.child.resolve() return self - def signature(self, depth=()): - return self.child.signature(depth) + def signature(self, schema=None, standalone=True): + name = create_prefixed_name(self.qname, schema) + if standalone: + return '%s(%s)' % ( + name, self.child.signature(schema, standalone=False)) + else: + return self.child.signature(schema, standalone=False) diff --git a/src/zeep/xsd/elements/references.py b/src/zeep/xsd/elements/references.py index d3eb1ce..4aa30b5 100644 --- a/src/zeep/xsd/elements/references.py +++ b/src/zeep/xsd/elements/references.py @@ -1,8 +1,15 @@ +""" +zeep.xsd.elements.references +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Ref* objecs are only used temporarily between parsing the schema and resolving +all the elements. + +""" __all__ = ['RefElement', 'RefAttribute', 'RefAttributeGroup', 'RefGroup'] class RefElement(object): - def __init__(self, tag, ref, schema, is_qualified=False, min_occurs=1, max_occurs=1): self._ref = ref @@ -37,4 +44,7 @@ class RefAttributeGroup(RefElement): class RefGroup(RefElement): def resolve(self): - return self._schema.get_group(self._ref) + elm = self._schema.get_group(self._ref) + elm = elm.clone( + elm.qname, min_occurs=self.min_occurs, max_occurs=self.max_occurs) + return elm diff --git a/src/zeep/xsd/schema.py b/src/zeep/xsd/schema.py index 93993c1..8026009 100644 --- a/src/zeep/xsd/schema.py +++ b/src/zeep/xsd/schema.py @@ -3,8 +3,7 @@ from collections import OrderedDict from lxml import etree -from zeep import exceptions -from zeep.xsd import const +from zeep import exceptions, ns from zeep.xsd.elements import builtins as xsd_builtins_elements from zeep.xsd.types import builtins as xsd_builtins_types from zeep.xsd.visitor import SchemaVisitor @@ -15,13 +14,24 @@ logger = logging.getLogger(__name__) class Schema(object): """A schema is a collection of schema documents.""" - def __init__(self, node=None, transport=None, location=None): + def __init__(self, node=None, transport=None, location=None, strict=True): + """ + :param node: + :param transport: + :param location: + :param strict: Boolean to indicate if the parsing is strict (default) + + """ + self.strict = strict + self._transport = transport self._documents = OrderedDict() self._prefix_map_auto = {} self._prefix_map_custom = {} + self._load_default_documents() + if not isinstance(node, list): nodes = [node] if node is not None else [] else: @@ -44,6 +54,12 @@ class Schema(object): }) return retval + @property + def root_document(self): + return next( + (doc for doc in self.documents if not doc._is_internal), + None) + @property def is_empty(self): """Boolean to indicate if this schema contains any types or elements""" @@ -55,25 +71,38 @@ class Schema(object): @property def elements(self): - """Yield all globla xsd.Type objects""" + """Yield all globla xsd.Type objects + + :rtype: Iterable of zeep.xsd.Element + + """ + seen = set() for document in self.documents: for element in document._elements.values(): - yield element + if element.qname not in seen: + yield element + seen.add(element.qname) @property def types(self): - """Yield all globla xsd.Type objects""" + """Yield all global xsd.Type objects + + :rtype: Iterable of zeep.xsd.ComplexType + + """ + seen = set() for document in self.documents: for type_ in document._types.values(): - yield type_ + if type_.qname not in seen: + yield type_ + seen.add(type_.qname) def __repr__(self): - if self._documents: - main_doc = next(self.documents) - location = main_doc._location - else: - location = '' - return '' % location + main_doc = self.root_document + if main_doc: + return '' % ( + main_doc._location, main_doc._target_namespace) + return '' def add_documents(self, schema_nodes, location): documents = [] @@ -87,49 +116,51 @@ class Schema(object): self._prefix_map_auto = self._create_prefix_map() def get_element(self, qname): - """Return a global xsd.Element object with the given qname""" + """Return a global xsd.Element object with the given qname + + :rtype: zeep.xsd.Group + + """ qname = self._create_qname(qname) - if qname.text in xsd_builtins_elements.default_elements: - return xsd_builtins_elements.default_elements[qname] - - # Handle XSD namespace items - if qname.namespace == const.NS_XSD: - try: - return xsd_builtins_elements.default_elements[qname] - except KeyError: - raise exceptions.LookupError("No such type %r" % qname.text) - return self._get_instance(qname, 'get_element', 'element') def get_type(self, qname, fail_silently=False): - """Return a global xsd.Type object with the given qname""" + """Return a global xsd.Type object with the given qname + + :rtype: zeep.xsd.ComplexType or zeep.xsd.AnySimpleType + + """ qname = self._create_qname(qname) - - # Handle XSD namespace items - if qname.namespace == const.NS_XSD: - try: - return xsd_builtins_types.default_types[qname] - except KeyError: - raise exceptions.LookupError("No such type %r" % qname.text) - try: return self._get_instance(qname, 'get_type', 'type') except exceptions.NamespaceError as exc: if fail_silently: - logger.info(str(exc)) + logger.debug(str(exc)) else: raise def get_group(self, qname): - """Return a global xsd.Group object with the given qname""" + """Return a global xsd.Group object with the given qname. + + :rtype: zeep.xsd.Group + + """ return self._get_instance(qname, 'get_group', 'group') def get_attribute(self, qname): - """Return a global xsd.attributeGroup object with the given qname""" + """Return a global xsd.attributeGroup object with the given qname + + :rtype: zeep.xsd.Attribute + + """ return self._get_instance(qname, 'get_attribute', 'attribute') def get_attribute_group(self, qname): - """Return a global xsd.attributeGroup object with the given qname""" + """Return a global xsd.attributeGroup object with the given qname + + :rtype: zeep.xsd.AttributeGroup + + """ return self._get_instance(qname, 'get_attribute_group', 'attributeGroup') def set_ns_prefix(self, prefix, namespace): @@ -144,6 +175,18 @@ class Schema(object): except KeyError: raise ValueError("No such prefix %r" % prefix) + def get_shorthand_for_ns(self, namespace): + for prefix, other_namespace in self._prefix_map_auto.items(): + if namespace == other_namespace: + return prefix + for prefix, other_namespace in self._prefix_map_custom.items(): + if namespace == other_namespace: + return prefix + + if namespace == 'http://schemas.xmlsoap.org/soap/envelope/': + return 'soap-env' + return namespace + def create_new_document(self, node, url, base_url=None): namespace = node.get('targetNamespace') if node is not None else None if base_url is None: @@ -160,6 +203,21 @@ class Schema(object): self._add_schema_document(document) self._prefix_map_auto = self._create_prefix_map() + def _load_default_documents(self): + schema = SchemaDocument(ns.XSD, None, None) + + for cls in xsd_builtins_types._types: + instance = cls(is_global=True) + schema.register_type(cls._default_qname, instance) + + for cls in xsd_builtins_elements._elements: + instance = cls() + schema.register_element(cls.qname, instance) + + schema._is_internal = True + self._add_schema_document(schema) + return schema + def _get_instance(self, qname, method_name, name): """Return an object from one of the SchemaDocument's""" qname = self._create_qname(qname) @@ -185,6 +243,8 @@ class Schema(object): This also expands the shorthand notation. + :rtype: lxml.etree.QNaame + """ if isinstance(name, etree.QName): return name @@ -205,26 +265,45 @@ class Schema(object): prefix_map = { 'xsd': 'http://www.w3.org/2001/XMLSchema', } - for i, namespace in enumerate(self._documents.keys()): - if namespace is None: + i = 0 + for namespace in self._documents.keys(): + if namespace is None or namespace in prefix_map.values(): continue + prefix_map['ns%d' % i] = namespace + i += 1 return prefix_map def _has_schema_document(self, namespace): + """Return a boolean if there is a SchemaDocumnet for the namespace. + + :rtype: boolean + + """ return namespace in self._documents def _add_schema_document(self, document): - logger.info("Add document with tns %s to schema %s", document.namespace, id(self)) + logger.debug("Add document with tns %s to schema %s", document.namespace, id(self)) documents = self._documents.setdefault(document.namespace, []) documents.append(document) def _get_schema_document(self, namespace, location): + """Return a list of SchemaDocument's for the given namespace AND + location. + + :rtype: SchemaDocument + + """ for document in self._documents.get(namespace, []): if document._location == location: return document def _get_schema_documents(self, namespace, fail_silently=False): + """Return a list of SchemaDocument's for the given namespace. + + :rtype: list of SchemaDocument + + """ if namespace not in self._documents: if fail_silently: return [] @@ -241,7 +320,7 @@ class SchemaDocument(object): self._base_url = base_url or location self._location = location self._target_namespace = namespace - self._elm_instances = [] + self._is_internal = False self._attribute_groups = {} self._attributes = {} @@ -283,7 +362,7 @@ class SchemaDocument(object): visitor.visit_schema(node) def resolve(self): - logger.info("Resolving in schema %s", self) + logger.debug("Resolving in schema %s", self) if self._resolved: return @@ -294,10 +373,23 @@ class SchemaDocument(object): schema.resolve() def _resolve_dict(val): - for key, obj in val.items(): - new = obj.resolve() - assert new is not None, "resolve() should return an object" - val[key] = new + try: + for key, obj in val.items(): + new = obj.resolve() + assert new is not None, "resolve() should return an object" + val[key] = new + except exceptions.LookupError as exc: + raise exceptions.LookupError( + ( + "Unable to resolve %(item_name)s %(qname)s in " + "%(file)s. (via %(parent)s)" + ) % { + 'item_name': exc.item_name, + 'item_name': exc.item_name, + 'qname': exc.qname, + 'file': exc.location, + 'parent': obj.qname, + }) _resolve_dict(self._attribute_groups) _resolve_dict(self._attributes) @@ -305,10 +397,6 @@ class SchemaDocument(object): _resolve_dict(self._groups) _resolve_dict(self._types) - for element in self._elm_instances: - element.resolve() - self._elm_instances = [] - def register_import(self, namespace, schema): schemas = self._imports.setdefault(namespace, []) schemas.append(schema) @@ -350,23 +438,43 @@ class SchemaDocument(object): self._attribute_groups[name] = value def get_type(self, qname): - """Return a xsd.Type object from this schema""" + """Return a xsd.Type object from this schema + + :rtype: zeep.xsd.ComplexType or zeep.xsd.AnySimpleType + + """ return self._get_instance(qname, self._types, 'type') def get_element(self, qname): - """Return a xsd.Element object from this schema""" + """Return a xsd.Element object from this schema + + :rtype: zeep.xsd.Element + + """ return self._get_instance(qname, self._elements, 'element') def get_group(self, qname): - """Return a xsd.Group object from this schema""" + """Return a xsd.Group object from this schema. + + :rtype: zeep.xsd.Group + + """ return self._get_instance(qname, self._groups, 'group') def get_attribute(self, qname): - """Return a xsd.Attribute object from this schema""" + """Return a xsd.Attribute object from this schema + + :rtype: zeep.xsd.Attribute + + """ return self._get_instance(qname, self._attributes, 'attribute') def get_attribute_group(self, qname): - """Return a xsd.AttributeGroup object from this schema""" + """Return a xsd.AttributeGroup object from this schema + + :rtype: zeep.xsd.AttributeGroup + + """ return self._get_instance(qname, self._attribute_groups, 'attributeGroup') def _get_instance(self, qname, items, item_name): @@ -377,10 +485,13 @@ class SchemaDocument(object): raise exceptions.LookupError(( "No %(item_name)s '%(localname)s' in namespace %(namespace)s. " + "Available %(item_name_plural)s are: %(known_items)s" - ) % { - 'item_name': item_name, - 'item_name_plural': item_name + 's', - 'localname': qname.localname, - 'namespace': qname.namespace, - 'known_items': known_items or ' - ' - }) + ) % { + 'item_name': item_name, + 'item_name_plural': item_name + 's', + 'localname': qname.localname, + 'namespace': qname.namespace, + 'known_items': known_items or ' - ' + }, + qname=qname, + item_name=item_name, + location=self._location) diff --git a/src/zeep/xsd/types/any.py b/src/zeep/xsd/types/any.py index a667b61..c5e994d 100644 --- a/src/zeep/xsd/types/any.py +++ b/src/zeep/xsd/types/any.py @@ -12,6 +12,11 @@ __all__ = ['AnyType'] class AnyType(Type): _default_qname = xsd_ns('anyType') + _attributes_unwrapped = [] + _element = None + + def __call__(self, value=None): + return value or '' def render(self, parent, value, xsd_type=None, render_path=None): if isinstance(value, AnyObject): @@ -27,7 +32,22 @@ class AnyType(Type): parent.text = self.xmlvalue(value) def parse_xmlelement(self, xmlelement, schema=None, allow_none=True, - context=None): + context=None, schema_type=None): + """Consume matching xmlelements and call parse() on each + + :param xmlelement: XML element objects + :type xmlelement: lxml.etree._Element + :param schema: The parent XML schema + :type schema: zeep.xsd.Schema + :param allow_none: Allow none + :type allow_none: bool + :param context: Optional parsing context (for inline schemas) + :type context: zeep.xsd.context.XmlParserContext + :param schema_type: The original type (not overriden via xsi:type) + :type schema_type: zeep.xsd.types.base.Type + :rtype: dict or None + + """ xsi_type = qname_attr(xmlelement, xsi_ns('type')) xsi_nil = xmlelement.get(xsi_ns('nil')) children = list(xmlelement.getchildren()) @@ -42,8 +62,14 @@ class AnyType(Type): xsd_type = schema.get_type(xsi_type, fail_silently=True) # If we were unable to resolve a type for the xsi:type (due to - # buggy soap servers) then we just return the lxml element. + # buggy soap servers) then we just return the text or lxml element. if not xsd_type: + logger.debug( + "Unable to resolve type for %r, returning raw data", + xsi_type.text) + + if xmlelement.text: + return xmlelement.text return children # If the xsd_type is xsd:anyType then we will recurs so ignore @@ -72,3 +98,6 @@ class AnyType(Type): def pythonvalue(self, value, schema=None): return value + + def signature(self, schema=None, standalone=True): + return 'xsd:anyType' diff --git a/src/zeep/xsd/types/base.py b/src/zeep/xsd/types/base.py index d67cb85..f8a2233 100644 --- a/src/zeep/xsd/types/base.py +++ b/src/zeep/xsd/types/base.py @@ -1,3 +1,8 @@ +from zeep.xsd.utils import create_prefixed_name + +__all__ = ['Type'] + + class Type(object): def __init__(self, qname=None, is_global=False): @@ -6,9 +11,16 @@ class Type(object): self._resolved = False self.is_global = is_global + def get_prefixed_name(self, schema): + return create_prefixed_name(self.qname, schema) + def accept(self, value): raise NotImplementedError + @property + def accepted_types(self): + return tuple() + def validate(self, value, required=False): return @@ -23,7 +35,7 @@ class Type(object): return {} def parse_xmlelement(self, xmlelement, schema=None, allow_none=True, - context=None): + context=None, schema_type=None): raise NotImplementedError( '%s.parse_xmlelement() is not implemented' % self.__class__.__name__) @@ -51,61 +63,5 @@ class Type(object): return [] @classmethod - def signature(cls, depth=()): + def signature(cls, schema=None, standalone=True): return '' - - -class UnresolvedType(Type): - def __init__(self, qname, schema): - self.qname = qname - assert self.qname.text != 'None' - self.schema = schema - - def __repr__(self): - return '<%s(qname=%r)>' % (self.__class__.__name__, self.qname) - - def render(self, parent, value, xsd_type=None, render_path=None): - raise RuntimeError( - "Unable to render unresolved type %s. This is probably a bug." % ( - self.qname)) - - def resolve(self): - retval = self.schema.get_type(self.qname) - return retval.resolve() - - -class UnresolvedCustomType(Type): - - def __init__(self, qname, base_type, schema): - assert qname is not None - self.qname = qname - self.name = str(qname.localname) - self.schema = schema - self.base_type = base_type - - def __repr__(self): - return '<%s(qname=%r, base_type=%r)>' % ( - self.__class__.__name__, self.qname.text, self.base_type) - - def resolve(self): - base = self.base_type - base = base.resolve() - - cls_attributes = { - '__module__': 'zeep.xsd.dynamic_types', - } - - from zeep.xsd.types.collection import UnionType # FIXME - from zeep.xsd.types.simple import AnySimpleType # FIXME - - if issubclass(base.__class__, UnionType): - xsd_type = type(self.name, (base.__class__,), cls_attributes) - return xsd_type(base.item_types) - - elif issubclass(base.__class__, AnySimpleType): - xsd_type = type(self.name, (base.__class__,), cls_attributes) - return xsd_type(self.qname) - - else: - xsd_type = type(self.name, (base.base_class,), cls_attributes) - return xsd_type(self.qname) diff --git a/src/zeep/xsd/types/builtins.py b/src/zeep/xsd/types/builtins.py index 49b8c3e..0bbe5d9 100644 --- a/src/zeep/xsd/types/builtins.py +++ b/src/zeep/xsd/types/builtins.py @@ -17,6 +17,11 @@ class ParseError(ValueError): pass +class BuiltinType(object): + def __init__(self, qname=None, is_global=False): + super(BuiltinType, self).__init__(qname, is_global=True) + + def check_no_collection(func): def _wrapper(self, value): if isinstance(value, (list, dict, set)): @@ -30,7 +35,7 @@ def check_no_collection(func): ## # Primitive types -class String(AnySimpleType): +class String(BuiltinType, AnySimpleType): _default_qname = xsd_ns('string') accepted_types = six.string_types @@ -44,13 +49,13 @@ class String(AnySimpleType): return value -class Boolean(AnySimpleType): +class Boolean(BuiltinType, AnySimpleType): _default_qname = xsd_ns('boolean') accepted_types = (bool,) @check_no_collection def xmlvalue(self, value): - return 'true' if value else 'false' + return 'true' if value and value not in ('false', '0') else 'false' def pythonvalue(self, value): """Return True if the 'true' or '1'. 'false' and '0' are legal false @@ -60,7 +65,7 @@ class Boolean(AnySimpleType): return value in ('true', '1') -class Decimal(AnySimpleType): +class Decimal(BuiltinType, AnySimpleType): _default_qname = xsd_ns('decimal') accepted_types = (_Decimal, float) + six.string_types @@ -72,7 +77,7 @@ class Decimal(AnySimpleType): return _Decimal(value) -class Float(AnySimpleType): +class Float(BuiltinType, AnySimpleType): _default_qname = xsd_ns('float') accepted_types = (float, _Decimal) + six.string_types @@ -83,7 +88,7 @@ class Float(AnySimpleType): return float(value) -class Double(AnySimpleType): +class Double(BuiltinType, AnySimpleType): _default_qname = xsd_ns('double') accepted_types = (_Decimal, float) + six.string_types @@ -95,7 +100,7 @@ class Double(AnySimpleType): return float(value) -class Duration(AnySimpleType): +class Duration(BuiltinType, AnySimpleType): _default_qname = xsd_ns('duration') accepted_types = (isodate.duration.Duration,) + six.string_types @@ -107,7 +112,7 @@ class Duration(AnySimpleType): return isodate.parse_duration(value) -class DateTime(AnySimpleType): +class DateTime(BuiltinType, AnySimpleType): _default_qname = xsd_ns('dateTime') accepted_types = (datetime.datetime,) + six.string_types @@ -131,7 +136,7 @@ class DateTime(AnySimpleType): return isodate.parse_datetime(value) -class Time(AnySimpleType): +class Time(BuiltinType, AnySimpleType): _default_qname = xsd_ns('time') accepted_types = (datetime.time,) + six.string_types @@ -145,7 +150,7 @@ class Time(AnySimpleType): return isodate.parse_time(value) -class Date(AnySimpleType): +class Date(BuiltinType, AnySimpleType): _default_qname = xsd_ns('date') accepted_types = (datetime.date,) + six.string_types @@ -159,7 +164,7 @@ class Date(AnySimpleType): return isodate.parse_date(value) -class gYearMonth(AnySimpleType): +class gYearMonth(BuiltinType, AnySimpleType): """gYearMonth represents a specific gregorian month in a specific gregorian year. @@ -186,7 +191,7 @@ class gYearMonth(AnySimpleType): _parse_timezone(group['timezone'])) -class gYear(AnySimpleType): +class gYear(BuiltinType, AnySimpleType): """gYear represents a gregorian calendar year. Lexical representation: CCYY @@ -209,7 +214,7 @@ class gYear(AnySimpleType): return (int(group['year']), _parse_timezone(group['timezone'])) -class gMonthDay(AnySimpleType): +class gMonthDay(BuiltinType, AnySimpleType): """gMonthDay is a gregorian date that recurs, specifically a day of the year such as the third of May. @@ -237,7 +242,7 @@ class gMonthDay(AnySimpleType): _parse_timezone(group['timezone'])) -class gDay(AnySimpleType): +class gDay(BuiltinType, AnySimpleType): """gDay is a gregorian day that recurs, specifically a day of the month such as the 5th of the month @@ -261,7 +266,7 @@ class gDay(AnySimpleType): return (int(group['day']), _parse_timezone(group['timezone'])) -class gMonth(AnySimpleType): +class gMonth(BuiltinType, AnySimpleType): """gMonth is a gregorian month that recurs every year. Lexical representation: --MM @@ -284,7 +289,7 @@ class gMonth(AnySimpleType): return (int(group['month']), _parse_timezone(group['timezone'])) -class HexBinary(AnySimpleType): +class HexBinary(BuiltinType, AnySimpleType): accepted_types = six.string_types _default_qname = xsd_ns('hexBinary') @@ -296,7 +301,7 @@ class HexBinary(AnySimpleType): return value -class Base64Binary(AnySimpleType): +class Base64Binary(BuiltinType, AnySimpleType): accepted_types = six.string_types _default_qname = xsd_ns('base64Binary') @@ -308,7 +313,7 @@ class Base64Binary(AnySimpleType): return base64.b64decode(value) -class AnyURI(AnySimpleType): +class AnyURI(BuiltinType, AnySimpleType): accepted_types = six.string_types _default_qname = xsd_ns('anyURI') @@ -320,7 +325,7 @@ class AnyURI(AnySimpleType): return value -class QName(AnySimpleType): +class QName(BuiltinType, AnySimpleType): accepted_types = six.string_types _default_qname = xsd_ns('QName') @@ -332,7 +337,7 @@ class QName(AnySimpleType): return value -class Notation(AnySimpleType): +class Notation(BuiltinType, AnySimpleType): accepted_types = six.string_types _default_qname = xsd_ns('NOTATION') @@ -390,6 +395,7 @@ class Entities(Entity): class Integer(Decimal): _default_qname = xsd_ns('integer') + accepted_types = (int, float) + six.string_types def xmlvalue(self, value): return str(value) @@ -484,58 +490,60 @@ def _unparse_timezone(tzinfo): return '-%02d:%02d' % (abs(hours), minutes) +_types = [ + # Primitive + String, + Boolean, + Decimal, + Float, + Double, + Duration, + DateTime, + Time, + Date, + gYearMonth, + gYear, + gMonthDay, + gDay, + gMonth, + HexBinary, + Base64Binary, + AnyURI, + QName, + Notation, + + # Derived + NormalizedString, + Token, + Language, + NmToken, + NmTokens, + Name, + NCName, + ID, + IDREF, + IDREFS, + Entity, + Entities, + Integer, + NonPositiveInteger, # noqa + NegativeInteger, + Long, + Int, + Short, + Byte, + NonNegativeInteger, # noqa + UnsignedByte, + UnsignedInt, + UnsignedLong, + UnsignedShort, + PositiveInteger, + + # Other + AnyType, + AnySimpleType, +] + default_types = { - cls._default_qname: cls() for cls in [ - # Primitive - String, - Boolean, - Decimal, - Float, - Double, - Duration, - DateTime, - Time, - Date, - gYearMonth, - gYear, - gMonthDay, - gDay, - gMonth, - HexBinary, - Base64Binary, - AnyURI, - QName, - Notation, - - # Derived - NormalizedString, - Token, - Language, - NmToken, - NmTokens, - Name, - NCName, - ID, - IDREF, - IDREFS, - Entity, - Entities, - Integer, - NonPositiveInteger, # noqa - NegativeInteger, - Long, - Int, - Short, - Byte, - NonNegativeInteger, # noqa - UnsignedByte, - UnsignedInt, - UnsignedLong, - UnsignedShort, - PositiveInteger, - - # Other - AnyType, - AnySimpleType, - ] + cls._default_qname: cls(is_global=True) for cls in _types } diff --git a/src/zeep/xsd/types/collection.py b/src/zeep/xsd/types/collection.py index f18b14c..5645f7d 100644 --- a/src/zeep/xsd/types/collection.py +++ b/src/zeep/xsd/types/collection.py @@ -32,8 +32,8 @@ class ListType(AnySimpleType): item_type = self.item_type return [item_type.pythonvalue(v) for v in value.split()] - def signature(self, depth=()): - return self.item_type.signature(depth) + '[]' + def signature(self, schema=None, standalone=True): + return self.item_type.signature(schema) + '[]' class UnionType(AnySimpleType): @@ -52,11 +52,11 @@ class UnionType(AnySimpleType): self.item_class = base_class return self - def signature(self, depth=()): + def signature(self, schema=None, standalone=True): return '' def parse_xmlelement(self, xmlelement, schema=None, allow_none=True, - context=None): + context=None, schema_type=None): if self.item_class: return self.item_class().parse_xmlelement( xmlelement, schema, allow_none, context) diff --git a/src/zeep/xsd/types/complex.py b/src/zeep/xsd/types/complex.py index c1b6925..7e65a70 100644 --- a/src/zeep/xsd/types/complex.py +++ b/src/zeep/xsd/types/complex.py @@ -6,14 +6,14 @@ from itertools import chain from cached_property import threaded_cached_property from zeep.exceptions import UnexpectedElementError, XMLParseError -from zeep.xsd.const import xsi_ns, SkipValue, NotSet +from zeep.xsd.const import NotSet, SkipValue, xsi_ns from zeep.xsd.elements import ( Any, AnyAttribute, AttributeGroup, Choice, Element, Group, Sequence) from zeep.xsd.elements.indicators import OrderIndicator from zeep.xsd.types.any import AnyType from zeep.xsd.types.simple import AnySimpleType from zeep.xsd.utils import NamePrefixGenerator -from zeep.xsd.valueobjects import CompoundValue +from zeep.xsd.valueobjects import CompoundValue, ArrayValue logger = logging.getLogger(__name__) @@ -33,14 +33,24 @@ class ComplexType(AnyType): self._attributes = attributes or [] self._restriction = restriction self._extension = extension + self._extension_types = tuple() super(ComplexType, self).__init__(qname=qname, is_global=is_global) def __call__(self, *args, **kwargs): + if self._array_type: + return self._array_class(*args, **kwargs) return self._value_class(*args, **kwargs) @property def accepted_types(self): - return (self._value_class,) + return (self._value_class,) + self._extension_types + + @threaded_cached_property + def _array_class(self): + assert self._array_type + return type( + self.__class__.__name__, (ArrayValue,), + {'_xsd_type': self, '__module__': 'zeep.objects'}) @threaded_cached_property def _value_class(self): @@ -94,25 +104,43 @@ class ComplexType(AnyType): generator = NamePrefixGenerator() # Handle wsdl:arrayType objects - attrs = {attr.qname.text: attr for attr in self._attributes if attr.qname} - array_type = attrs.get('{http://schemas.xmlsoap.org/soap/encoding/}arrayType') - if array_type: + if self._array_type: name = generator.get_name() if isinstance(self._element, Group): - return [(name, Sequence([ - Any(max_occurs='unbounded', restrict=array_type.array_type) + result = [(name, Sequence([ + Any(max_occurs='unbounded', restrict=self._array_type.array_type) ]))] else: - return [(name, self._element)] - - # _element is one of All, Choice, Group, Sequence - if self._element: - result.append((generator.get_name(), self._element)) + result = [(name, self._element)] + else: + # _element is one of All, Choice, Group, Sequence + if self._element: + result.append((generator.get_name(), self._element)) return result - def parse_xmlelement(self, xmlelement, schema, allow_none=True, - context=None): - """Consume matching xmlelements and call parse() on each""" + @property + def _array_type(self): + attrs = {attr.qname.text: attr for attr in self._attributes if attr.qname} + array_type = attrs.get('{http://schemas.xmlsoap.org/soap/encoding/}arrayType') + return array_type + + def parse_xmlelement(self, xmlelement, schema=None, allow_none=True, + context=None, schema_type=None): + """Consume matching xmlelements and call parse() on each + + :param xmlelement: XML element objects + :type xmlelement: lxml.etree._Element + :param schema: The parent XML schema + :type schema: zeep.xsd.Schema + :param allow_none: Allow none + :type allow_none: bool + :param context: Optional parsing context (for inline schemas) + :type context: zeep.xsd.context.XmlParserContext + :param schema_type: The original type (not overriden via xsi:type) + :type schema_type: zeep.xsd.types.base.Type + :rtype: dict or None + + """ # If this is an empty complexType () if not self.attributes and not self.elements: return None @@ -134,6 +162,7 @@ class ComplexType(AnyType): # Parse elements. These are always indicator elements (all, choice, # group, sequence) + assert len(self.elements_nested) < 2 for name, element in self.elements_nested: try: result = element.parse_xmlelements( @@ -145,7 +174,10 @@ class ComplexType(AnyType): # Check if all children are consumed (parsed) if elements: - raise XMLParseError("Unexpected element %r" % elements[0].tag) + if schema.strict: + raise XMLParseError("Unexpected element %r" % elements[0].tag) + else: + init_kwargs['_raw_elements'] = elements # Parse attributes if attributes: @@ -158,12 +190,21 @@ class ComplexType(AnyType): else: init_kwargs[name] = attribute.parse(attributes) - return self(**init_kwargs) + value = self._value_class(**init_kwargs) + schema_type = schema_type or self + if schema_type and getattr(schema_type, '_array_type', None): + value = schema_type._array_class.from_value_object(value) + return value def render(self, parent, value, xsd_type=None, render_path=None): """Serialize the given value lxml.Element subelements on the parent element. + :type parent: lxml.etree._Element + :type value: Union[list, dict, zeep.xsd.valueobjects.CompoundValue] + :type xsd_type: zeep.xsd.types.base.Type + :param render_path: list + """ if not render_path: render_path = [self.name] @@ -171,12 +212,24 @@ class ComplexType(AnyType): if not self.elements_nested and not self.attributes: return + if isinstance(value, ArrayValue): + value = value.as_value_object() + # Render attributes for name, attribute in self.attributes: attr_value = value[name] if name in value else NotSet child_path = render_path + [name] attribute.render(parent, attr_value, child_path) + if ( + len(self.elements_nested) == 1 + and isinstance(value, self.accepted_types) + and not isinstance(value, (list, dict, CompoundValue)) + ): + element = self.elements_nested[0][1] + element.type.render(parent, value, None, child_path) + return + # Render sub elements for name, element in self.elements_nested: if isinstance(element, Element) or element.accepts_multiple: @@ -186,6 +239,7 @@ class ComplexType(AnyType): element_value = value child_path = list(render_path) + # We want to explicitly skip this sub-element if element_value is SkipValue: continue @@ -201,6 +255,19 @@ class ComplexType(AnyType): parent.set(xsi_ns('type'), xsd_type.qname) def parse_kwargs(self, kwargs, name, available_kwargs): + """Parse the kwargs for this type and return the accepted data as + a dict. + + :param kwargs: The kwargs + :type kwargs: dict + :param name: The name as which this type is registered in the parent + :type name: str + :param available_kwargs: The kwargs keys which are still available, + modified in place + :type available_kwargs: set + :rtype: dict + + """ value = None name = name or self.name @@ -213,28 +280,27 @@ class ComplexType(AnyType): return {} def _create_object(self, value, name): - """Return the value as a CompoundValue object""" + """Return the value as a CompoundValue object + + :type value: str + :type value: list, dict, CompoundValue + + """ if value is None: return None - if isinstance(value, list): + if isinstance(value, list) and not self._array_type: return [self._create_object(val, name) for val in value] - if isinstance(value, CompoundValue): + if isinstance(value, CompoundValue) or value is SkipValue: return value if isinstance(value, dict): return self(**value) - # Check if the valueclass only expects one value, in that case - # we can try to automatically create an object for it. - if len(self.attributes) + len(self.elements) == 1: - return self(value) - - raise ValueError(( - "Error while create XML for complexType '%s': " - "Expected instance of type %s, received %r instead." - ) % (self.qname or name, self._value_class, type(value))) + # Try to automatically create an object. This might fail if there + # are multiple required arguments. + return self(value) def resolve(self): """Resolve all sub elements and types""" @@ -242,9 +308,6 @@ class ComplexType(AnyType): return self._resolved self._resolved = self - if self._element: - self._element = self._element.resolve() - resolved = [] for attribute in self._attributes: value = attribute.resolve() @@ -258,18 +321,17 @@ class ComplexType(AnyType): if self._extension: self._extension = self._extension.resolve() self._resolved = self.extend(self._extension) - return self._resolved - elif self._restriction: self._restriction = self._restriction.resolve() self._resolved = self.restrict(self._restriction) - return self._resolved - else: - return self._resolved + if self._element: + self._element = self._element.resolve() + + return self._resolved def extend(self, base): - """Create a new complextype instance which is the current type + """Create a new ComplexType instance which is the current type extending the given base type. Used for handling xsd:extension tags @@ -277,6 +339,9 @@ class ComplexType(AnyType): TODO: Needs a rewrite where the child containers are responsible for the extend functionality. + :type base: zeep.xsd.types.base.Type + :rtype base: zeep.xsd.types.base.Type + """ if isinstance(base, ComplexType): base_attributes = base._attributes_unwrapped @@ -301,6 +366,9 @@ class ComplexType(AnyType): # container a placeholder element). element = [] if self._element and base_element: + self._element = self._element.resolve() + base_element = base_element.resolve() + element = self._element.clone(self._element.name) if isinstance(base_element, OrderIndicator): if isinstance(self._element, Choice): @@ -323,7 +391,10 @@ class ComplexType(AnyType): new = self.__class__( element=element, attributes=attributes, - qname=self.qname) + qname=self.qname, + is_global=self.is_global) + + new._extension_types = base.accepted_types return new def restrict(self, base): @@ -332,6 +403,10 @@ class ComplexType(AnyType): Used for handling xsd:restriction + :type base: zeep.xsd.types.base.Type + :rtype base: zeep.xsd.types.base.Type + + """ attributes = list( chain(base._attributes_unwrapped, self._attributes_unwrapped)) @@ -344,33 +419,30 @@ class ComplexType(AnyType): new_attributes['##any'] = attr else: new_attributes[attr.qname.text] = attr - attributes = new_attributes.values() + attributes = list(new_attributes.values()) + + if base._element: + base._element.resolve() new = self.__class__( element=self._element or base._element, attributes=attributes, - qname=self.qname) + qname=self.qname, + is_global=self.is_global) return new.resolve() - def signature(self, depth=()): - if len(depth) > 0 and self.is_global: - return self.name - + def signature(self, schema=None, standalone=True): parts = [] - depth += (self.name,) for name, element in self.elements_nested: - # http://schemas.xmlsoap.org/soap/encoding/ contains cyclic type - if isinstance(element, Element) and element.type == self: - continue - - part = element.signature(depth) + part = element.signature(schema, standalone=False) parts.append(part) for name, attribute in self.attributes: - part = '%s: %s' % (name, attribute.signature(depth)) + part = '%s: %s' % (name, attribute.signature(schema, standalone=False)) parts.append(part) value = ', '.join(parts) - if len(depth) > 1: - value = '{%s}' % value - return value + if standalone: + return '%s(%s)' % (self.get_prefixed_name(schema), value) + else: + return value diff --git a/src/zeep/xsd/types/simple.py b/src/zeep/xsd/types/simple.py index 989d292..4c5440a 100644 --- a/src/zeep/xsd/types/simple.py +++ b/src/zeep/xsd/types/simple.py @@ -4,7 +4,7 @@ import six from lxml import etree from zeep.exceptions import ValidationError -from zeep.xsd.const import NS_XSD, xsd_ns +from zeep.xsd.const import xsd_ns from zeep.xsd.types.any import AnyType logger = logging.getLogger(__name__) @@ -54,7 +54,7 @@ class AnySimpleType(AnyType): return '%s(value)' % (self.__class__.__name__) def parse_xmlelement(self, xmlelement, schema=None, allow_none=True, - context=None): + context=None, schema_type=None): if xmlelement.text is None: return try: @@ -70,10 +70,8 @@ class AnySimpleType(AnyType): def render(self, parent, value, xsd_type=None, render_path=None): parent.text = self.xmlvalue(value) - def signature(self, depth=()): - if self.qname.namespace == NS_XSD: - return 'xsd:%s' % self.name - return self.name + def signature(self, schema=None, standalone=True): + return self.get_prefixed_name(schema) def validate(self, value, required=False): if required and value is None: diff --git a/src/zeep/xsd/types/unresolved.py b/src/zeep/xsd/types/unresolved.py new file mode 100644 index 0000000..e15a03e --- /dev/null +++ b/src/zeep/xsd/types/unresolved.py @@ -0,0 +1,56 @@ +from zeep.xsd.types.base import Type +from zeep.xsd.types.collection import UnionType # FIXME +from zeep.xsd.types.simple import AnySimpleType # FIXME + + +class UnresolvedType(Type): + def __init__(self, qname, schema): + self.qname = qname + assert self.qname.text != 'None' + self.schema = schema + + def __repr__(self): + return '<%s(qname=%r)>' % (self.__class__.__name__, self.qname.text) + + def render(self, parent, value, xsd_type=None, render_path=None): + raise RuntimeError( + "Unable to render unresolved type %s. This is probably a bug." % ( + self.qname)) + + def resolve(self): + retval = self.schema.get_type(self.qname) + return retval.resolve() + + +class UnresolvedCustomType(Type): + + def __init__(self, qname, base_type, schema): + assert qname is not None + self.qname = qname + self.name = str(qname.localname) + self.schema = schema + self.base_type = base_type + + def __repr__(self): + return '<%s(qname=%r, base_type=%r)>' % ( + self.__class__.__name__, self.qname.text, self.base_type) + + def resolve(self): + base = self.base_type + base = base.resolve() + + cls_attributes = { + '__module__': 'zeep.xsd.dynamic_types', + } + + if issubclass(base.__class__, UnionType): + xsd_type = type(self.name, (base.__class__,), cls_attributes) + return xsd_type(base.item_types) + + elif issubclass(base.__class__, AnySimpleType): + xsd_type = type(self.name, (base.__class__,), cls_attributes) + return xsd_type(self.qname) + + else: + xsd_type = type(self.name, (base.base_class,), cls_attributes) + return xsd_type(self.qname) diff --git a/src/zeep/xsd/utils.py b/src/zeep/xsd/utils.py index 2b5a3cc..e60c8f6 100644 --- a/src/zeep/xsd/utils.py +++ b/src/zeep/xsd/utils.py @@ -1,10 +1,6 @@ -from defusedxml.lxml import fromstring -from lxml import etree - from six.moves import range -from six.moves.urllib.parse import urlparse -from zeep.exceptions import XMLSyntaxError -from zeep.parser import absolute_location + +from zeep import ns class NamePrefixGenerator(object): @@ -31,34 +27,6 @@ class UniqueNameGenerator(object): return name -class ImportResolver(etree.Resolver): - """Custom lxml resolve to use the transport object""" - def __init__(self, transport): - self.transport = transport - - def resolve(self, url, pubid, context): - if urlparse(url).scheme in ('http', 'https'): - content = self.transport.load(url) - return self.resolve_string(content, context) - - -def parse_xml(content, transport, base_url=None): - parser = etree.XMLParser(remove_comments=True, resolve_entities=False) - parser.resolvers.add(ImportResolver(transport)) - try: - return fromstring(content, parser=parser, base_url=base_url) - except etree.XMLSyntaxError as exc: - raise XMLSyntaxError("Invalid XML content received (%s)" % exc.message) - - -def load_external(url, transport, base_url=None): - if base_url: - url = absolute_location(url, base_url) - - response = transport.load(url) - return parse_xml(response, transport, base_url) - - def max_occurs_iter(max_occurs, items=None): assert max_occurs is not None generator = range(0, max_occurs if max_occurs != 'unbounded' else 2**31-1) @@ -69,3 +37,27 @@ def max_occurs_iter(max_occurs, items=None): else: for i in generator: yield i + + +def create_prefixed_name(qname, schema): + """Convert a QName to a xsd:name ('ns1:myType'). + + :type qname: lxml.etree.QName + :type schema: zeep.xsd.schema.Schema + :rtype: str + + """ + if not qname: + return + + if schema and qname.namespace: + prefix = schema.get_shorthand_for_ns(qname.namespace) + if prefix: + return '%s:%s' % (prefix, qname.localname) + elif qname.namespace in ns.NAMESPACE_TO_PREFIX: + prefix = ns.NAMESPACE_TO_PREFIX[qname.namespace] + return '%s:%s' % (prefix, qname.localname) + + if qname.namespace: + return qname.text + return qname.localname diff --git a/src/zeep/xsd/valueobjects.py b/src/zeep/xsd/valueobjects.py index c2cf9c5..7ba86d8 100644 --- a/src/zeep/xsd/valueobjects.py +++ b/src/zeep/xsd/valueobjects.py @@ -33,11 +33,46 @@ class AnyObject(object): return self.xsd_obj +def _unpickle_compound_value(name, values): + """Helper function to recreate pickled CompoundValue. + + See CompoundValue.__reduce__ + + """ + cls = type(name, (CompoundValue,), { + '_xsd_type': None, '__module__': 'zeep.objects' + }) + obj = cls() + obj.__values__ = values + return obj + + +class ArrayValue(list): + def __init__(self, items): + super(ArrayValue, self).__init__(items) + + def as_value_object(self): + anon_type = type( + self.__class__.__name__, (CompoundValue,), + {'_xsd_type': self._xsd_type, '__module__': 'zeep.objects'}) + return anon_type(list(self)) + + @classmethod + def from_value_object(cls, obj): + items = next(iter(obj.__values__.values())) + return cls(items or []) + + class CompoundValue(object): + """Represents a data object for a specific xsd:complexType.""" def __init__(self, *args, **kwargs): values = OrderedDict() + # Can be done after unpickle + if self._xsd_type is None: + return + # Set default values for container_name, container in self._xsd_type.elements_nested: elm_values = container.default_value @@ -56,6 +91,9 @@ class CompoundValue(object): values[key] = value self.__values__ = values + def __reduce__(self): + return (_unpickle_compound_value, (self.__class__.__name__, self.__values__,)) + def __contains__(self, key): return self.__values__.__contains__(key) @@ -107,6 +145,9 @@ class CompoundValue(object): setattr(new, attr, value) return new + def __json__(self): + return self.__values__ + def _process_signature(xsd_type, args, kwargs): """Return a dict with the args/kwargs mapped to the field name. @@ -169,13 +210,19 @@ def _process_signature(xsd_type, args, kwargs): available_kwargs.remove(attribute_name) result[attribute_name] = kwargs[attribute_name] + # _raw_elements is a special kwarg used for unexpected unparseable xml + # elements (e.g. for soap:header or when strict is disabled) + if '_raw_elements' in available_kwargs and kwargs['_raw_elements']: + result['_raw_elements'] = kwargs['_raw_elements'] + available_kwargs.remove('_raw_elements') + if available_kwargs: raise TypeError(( "%s() got an unexpected keyword argument %r. " + - "Signature: (%s)" + "Signature: `%s`" ) % ( xsd_type.qname or 'ComplexType', next(iter(available_kwargs)), - xsd_type.signature())) + xsd_type.signature(standalone=False))) return result diff --git a/src/zeep/xsd/visitor.py b/src/zeep/xsd/visitor.py index 8ed0160..9ba0101 100644 --- a/src/zeep/xsd/visitor.py +++ b/src/zeep/xsd/visitor.py @@ -4,14 +4,13 @@ import re from lxml import etree -from zeep import exceptions from zeep.exceptions import XMLParseError -from zeep.parser import absolute_location +from zeep.loader import absolute_location, load_external from zeep.utils import as_qname, qname_attr from zeep.xsd import elements as xsd_elements from zeep.xsd import types as xsd_types from zeep.xsd.const import xsd_ns -from zeep.xsd.utils import load_external +from zeep.xsd.types.unresolved import UnresolvedCustomType, UnresolvedType logger = logging.getLogger(__name__) @@ -37,12 +36,36 @@ class SchemaVisitor(object): """Visitor which processes XSD files and registers global elements and types in the given schema. + :param schema: + :type schema: zeep.xsd.schema.Schema + :param document: + :type document: zeep.xsd.schema.SchemaDocument + """ + def __init__(self, schema, document): self.document = document self.schema = schema self._includes = set() + def register_element(self, qname, instance): + self.document.register_element(qname, instance) + + def register_attribute(self, name, instance): + self.document.register_attribute(name, instance) + + def register_type(self, qname, instance): + self.document.register_type(qname, instance) + + def register_group(self, qname, instance): + self.document.register_group(qname, instance) + + def register_attribute_group(self, qname, instance): + self.document.register_attribute_group(qname, instance) + + def register_import(self, namespace, document): + self.document.register_import(namespace, document) + def process(self, node, parent): visit_func = self.visitors.get(node.tag) if not visit_func: @@ -79,7 +102,10 @@ class SchemaVisitor(object): return cls(node.tag, ref, self.schema, **kwargs) def visit_schema(self, node): - """ + """Visit the xsd:schema element and process all the child elements + + Definition:: + + :param node: The XML node + :type node: lxml.etree._Element + """ assert node is not None @@ -104,12 +133,14 @@ class SchemaVisitor(object): self.document._element_form = node.get('elementFormDefault', 'unqualified') self.document._attribute_form = node.get('attributeFormDefault', 'unqualified') - parent = node - for node in node.iterchildren(): - self.process(node, parent=parent) + for child in node: + self.process(child, parent=node) def visit_import(self, node, parent): """ + + Definition:: + Content: (annotation?) + + :param node: The XML node + :type node: lxml.etree._Element + :param parent: The parent XML node + :type parent: lxml.etree._Element + """ schema_node = None namespace = node.get('namespace') @@ -136,7 +173,7 @@ class SchemaVisitor(object): document = self.schema._get_schema_document(namespace, location) if document: logger.debug("Returning existing schema: %r", location) - self.document.register_import(namespace, document) + self.register_import(namespace, document) return document # Hardcode the mapping between the xml namespace and the xsd for now. @@ -153,7 +190,10 @@ class SchemaVisitor(object): return # Load the XML - schema_node = load_external(location, self.schema._transport) + schema_node = load_external( + location, + self.schema._transport, + strict=self.schema.strict) # Check if the xsd:import namespace matches the targetNamespace. If # the xsd:import statement didn't specify a namespace then make sure @@ -168,17 +208,26 @@ class SchemaVisitor(object): sourceline=node.sourceline) schema = self.schema.create_new_document(schema_node, location) - self.document.register_import(namespace, schema) + self.register_import(namespace, schema) return schema def visit_include(self, node, parent): """ - - Content: (annotation?) - + + Definition:: + + + Content: (annotation?) + + + :param node: The XML node + :type node: lxml.etree._Element + :param parent: The parent XML node + :type parent: lxml.etree._Element + """ if not node.get('schemaLocation'): raise NotImplementedError("schemaLocation is required") @@ -188,13 +237,49 @@ class SchemaVisitor(object): return schema_node = load_external( - location, self.schema._transport, base_url=self.document._base_url) + location, self.schema._transport, + base_url=self.document._base_url, + strict=self.schema.strict) self._includes.add(location) - return self.visit_schema(schema_node) + # When the included document has no default namespace defined but the + # parent document does have this then we should (atleast for #360) + # transfer the default namespace to the included schema. We can't + # update the nsmap of elements in lxml so we create a new schema with + # the correct nsmap and move all the content there. + if not schema_node.nsmap.get(None) and node.nsmap.get(None): + nsmap = {None: node.nsmap[None]} + nsmap.update(schema_node.nsmap) + new = etree.Element(schema_node.tag, nsmap=nsmap) + for child in schema_node: + new.append(child) + for key, value in schema_node.attrib.items(): + new.set(key, value) + schema_node = new + + # Use the element/attribute form defaults from the schema while + # processing the nodes. + element_form_default = self.document._element_form + attribute_form_default = self.document._attribute_form + base_url = self.document._base_url + + self.document._element_form = schema_node.get('elementFormDefault', 'unqualified') + self.document._attribute_form = schema_node.get('attributeFormDefault', 'unqualified') + self.document._base_url = absolute_location(location, self.document._base_url) + + # Iterate directly over the children. + for child in schema_node: + self.process(child, parent=schema_node) + + self.document._element_form = element_form_default + self.document._attribute_form = attribute_form_default + self.document._base_url = base_url def visit_element(self, node, parent): """ + + Definition:: + + + :param node: The XML node + :type node: lxml.etree._Element + :param parent: The parent XML node + :type parent: lxml.etree._Element + """ is_global = parent.tag == tags.schema @@ -272,16 +363,16 @@ class SchemaVisitor(object): min_occurs=min_occurs, max_occurs=max_occurs, nillable=nillable, default=default, is_global=is_global) - self.document._elm_instances.append(element) - # Only register global elements if is_global: - self.document.register_element(qname, element) + self.register_element(qname, element) return element def visit_attribute(self, node, parent): """Declares an attribute. + Definition:: + Content: (annotation?, (simpleType?)) + + :param node: The XML node + :type node: lxml.etree._Element + :param parent: The parent XML node + :type parent: lxml.etree._Element + """ is_global = parent.tag == tags.schema @@ -303,9 +400,8 @@ class SchemaVisitor(object): match = re.match('([^\[]+)', array_type) if match: array_type = match.groups()[0] - qname = as_qname( - array_type, node.nsmap, self.document._target_namespace) - array_type = xsd_types.UnresolvedType(qname, self.schema) + qname = as_qname(array_type, node.nsmap) + array_type = UnresolvedType(qname, self.schema) # If the elment has a ref attribute then all other attributes cannot # be present. Short circuit that here. @@ -316,9 +412,8 @@ class SchemaVisitor(object): return result attribute_form = node.get('form', self.document._attribute_form) - qname = qname_attr(node, 'name', self.document._target_namespace) if attribute_form == 'qualified' or is_global: - name = qname + name = qname_attr(node, 'name', self.document._target_namespace) else: name = etree.QName(node.get('name')) @@ -338,15 +433,16 @@ class SchemaVisitor(object): attr = xsd_elements.Attribute( name, type_=xsd_type, default=default, required=required) - self.document._elm_instances.append(attr) # Only register global elements if is_global: - self.document.register_attribute(qname, attr) + self.register_attribute(name, attr) return attr def visit_simple_type(self, node, parent): """ + Definition:: + Content: (annotation?, (restriction | list | union)) + + :param node: The XML node + :type node: lxml.etree._Element + :param parent: The parent XML node + :type parent: lxml.etree._Element + """ if parent.tag == tags.schema: @@ -369,8 +471,7 @@ class SchemaVisitor(object): child = items[0] if child.tag == tags.restriction: base_type = self.visit_restriction_simple_type(child, node) - xsd_type = xsd_types.UnresolvedCustomType( - qname, base_type, self.schema) + xsd_type = UnresolvedCustomType(qname, base_type, self.schema) elif child.tag == tags.list: xsd_type = self.visit_list(child, node) @@ -382,23 +483,30 @@ class SchemaVisitor(object): assert xsd_type is not None if is_global: - self.document.register_type(qname, xsd_type) + self.register_type(qname, xsd_type) return xsd_type def visit_complex_type(self, node, parent): """ - - Content: (annotation?, (simpleContent | complexContent | - ((group | all | choice | sequence)?, - ((attribute | attributeGroup)*, anyAttribute?)))) - + Definition:: + + + Content: (annotation?, (simpleContent | complexContent | + ((group | all | choice | sequence)?, + ((attribute | attributeGroup)*, anyAttribute?)))) + + + :param node: The XML node + :type node: lxml.etree._Element + :param parent: The parent XML node + :type parent: lxml.etree._Element """ children = [] @@ -448,22 +556,30 @@ class SchemaVisitor(object): element=element, attributes=attributes, qname=qname, is_global=is_global) else: - xsd_type = xsd_cls(qname=qname) + xsd_type = xsd_cls(qname=qname, is_global=is_global) if is_global: - self.document.register_type(qname, xsd_type) + self.register_type(qname, xsd_type) return xsd_type - def visit_complex_content(self, node, parent, namespace=None): + def visit_complex_content(self, node, parent): """The complexContent element defines extensions or restrictions on a complex type that contains mixed content or elements only. + Definition:: + Content: (annotation?, (restriction | extension)) + + :param node: The XML node + :type node: lxml.etree._Element + :param parent: The parent XML node + :type parent: lxml.etree._Element + """ child = node.getchildren()[-1] @@ -485,16 +601,24 @@ class SchemaVisitor(object): 'extension': base, } - def visit_simple_content(self, node, parent, namespace=None): + def visit_simple_content(self, node, parent): """Contains extensions or restrictions on a complexType element with character data or a simpleType element as content and contains no elements. + Definition:: + Content: (annotation?, (restriction | extension)) + + :param node: The XML node + :type node: lxml.etree._Element + :param parent: The parent XML node + :type parent: lxml.etree._Element + """ child = node.getchildren()[-1] @@ -505,8 +629,10 @@ class SchemaVisitor(object): return self.visit_extension_simple_content(child, node) raise AssertionError("Expected restriction or extension") - def visit_restriction_simple_type(self, node, parent, namespace=None): + def visit_restriction_simple_type(self, node, parent): """ + Definition:: + + + :param node: The XML node + :type node: lxml.etree._Element + :param parent: The parent XML node + :type parent: lxml.etree._Element + """ base_name = qname_attr(node, 'base') if base_name: @@ -526,8 +658,10 @@ class SchemaVisitor(object): if children[0].tag == tags.simpleType: return self.visit_simple_type(children[0], node) - def visit_restriction_simple_content(self, node, parent, namespace=None): + def visit_restriction_simple_content(self, node, parent): """ + Definition:: + + + :param node: The XML node + :type node: lxml.etree._Element + :param parent: The parent XML node + :type parent: lxml.etree._Element + """ base_name = qname_attr(node, 'base') base_type = self._get_type(base_name) return base_type, [] - def visit_restriction_complex_content(self, node, parent, namespace=None): + def visit_restriction_complex_content(self, node, parent): """ + Definition:: + + + :param node: The XML node + :type node: lxml.etree._Element + :param parent: The parent XML node + :type parent: lxml.etree._Element + """ base_name = qname_attr(node, 'base') base_type = self._get_type(base_name) @@ -572,6 +720,9 @@ class SchemaVisitor(object): def visit_extension_complex_content(self, node, parent): """ + + Definition:: + + + :param node: The XML node + :type node: lxml.etree._Element + :param parent: The parent XML node + :type parent: lxml.etree._Element + """ base_name = qname_attr(node, 'base') base_type = self._get_type(base_name) @@ -599,6 +756,9 @@ class SchemaVisitor(object): def visit_extension_simple_content(self, node, parent): """ + + Definition:: + Content: (appinfo | documentation)* + + :param node: The XML node + :type node: lxml.etree._Element + :param parent: The parent XML node + :type parent: lxml.etree._Element + """ return def visit_any(self, node, parent): """ + + Definition:: + Content: (annotation?) + + :param node: The XML node + :type node: lxml.etree._Element + :param parent: The parent XML node + :type parent: lxml.etree._Element + """ min_occurs, max_occurs = _process_occurs_attrs(node) process_contents = node.get('processContents', 'strict') @@ -645,6 +822,8 @@ class SchemaVisitor(object): def visit_sequence(self, node, parent): """ + Definition:: + + + :param node: The XML node + :type node: lxml.etree._Element + :param parent: The parent XML node + :type parent: lxml.etree._Element + """ sub_types = [ @@ -677,6 +862,8 @@ class SchemaVisitor(object): """Allows the elements in the group to appear (or not appear) in any order in the containing element. + Definition:: + Content: (annotation?, element*) + + :param node: The XML node + :type node: lxml.etree._Element + :param parent: The parent XML node + :type parent: lxml.etree._Element + """ sub_types = [ @@ -703,6 +896,8 @@ class SchemaVisitor(object): """Groups a set of element declarations so that they can be incorporated as a group into complex type definitions. + Definition:: + - """ + :param node: The XML node + :type node: lxml.etree._Element + :param parent: The parent XML node + :type parent: lxml.etree._Element - result = self.process_reference(node) + """ + min_occurs, max_occurs = _process_occurs_attrs(node) + + result = self.process_reference( + node, min_occurs=min_occurs, max_occurs=max_occurs) if result: return result @@ -730,11 +932,13 @@ class SchemaVisitor(object): elm = xsd_elements.Group(name=qname, child=item) if parent.tag == tags.schema: - self.document.register_group(qname, elm) + self.register_group(qname, elm) return elm def visit_list(self, node, parent): """ + Definition:: + Content: (annotation?, (simpleType*)) + + :param node: The XML node + :type node: lxml.etree._Element + :param parent: The parent XML node + :type parent: lxml.etree._Element + """ # TODO members = node.get('memberTypes') types = [] if members: for member in members.split(): - qname = as_qname(member, node.nsmap, self.document._target_namespace) + qname = as_qname(member, node.nsmap) xsd_type = self._get_type(qname) types.append(xsd_type) else: @@ -805,18 +1025,28 @@ class SchemaVisitor(object): attribute or element values) must be unique within the specified scope. The value must be unique or nil. + Definition:: + Content: (annotation?, (selector, field+)) + + :param node: The XML node + :type node: lxml.etree._Element + :param parent: The parent XML node + :type parent: lxml.etree._Element + """ # TODO pass def visit_attribute_group(self, node, parent): """ + Definition:: + + + :param node: The XML node + :type node: lxml.etree._Element + :param parent: The parent XML node + :type parent: lxml.etree._Element + """ ref = self.process_reference(node) if ref: @@ -835,10 +1071,12 @@ class SchemaVisitor(object): attributes = self._process_attributes(node, children) attribute_group = xsd_elements.AttributeGroup(qname, attributes) - self.document.register_attribute_group(qname, attribute_group) + self.register_attribute_group(qname, attribute_group) def visit_any_attribute(self, node, parent): """ + Definition:: + Content: (annotation?) + + :param node: The XML node + :type node: lxml.etree._Element + :param parent: The parent XML node + :type parent: lxml.etree._Element + """ process_contents = node.get('processContents', 'strict') return xsd_elements.AnyAttribute(process_contents=process_contents) @@ -856,6 +1100,8 @@ class SchemaVisitor(object): non-XML data within an XML document. An XML Schema notation declaration is a reconstruction of XML 1.0 NOTATION declarations. + Definition:: + + :param node: The XML node + :type node: lxml.etree._Element + :param parent: The parent XML node + :type parent: lxml.etree._Element + """ pass def _get_type(self, name): assert name is not None name = self._create_qname(name) - try: - retval = self.schema.get_type(name) - except (exceptions.NamespaceError, exceptions.LookupError): - retval = xsd_types.UnresolvedType(name, self.schema) - return retval + return UnresolvedType(name, self.schema) def _create_qname(self, name): if not isinstance(name, etree.QName): diff --git a/tests/test_asyncio_transport.py b/tests/test_asyncio_transport.py index 2c65a45..7aca012 100644 --- a/tests/test_asyncio_transport.py +++ b/tests/test_asyncio_transport.py @@ -1,6 +1,7 @@ import pytest from pretend import stub from lxml import etree +import aiohttp from aioresponses import aioresponses from zeep import cache, asyncio @@ -39,3 +40,21 @@ async def test_post(event_loop): headers={}) assert result.content == b'x' + + +@pytest.mark.requests +@pytest.mark.asyncio +async def test_session_close(event_loop): + transport = asyncio.AsyncTransport(loop=event_loop) + session = transport.session # copy session object from transport + del transport + assert session.closed + + +@pytest.mark.requests +@pytest.mark.asyncio +async def test_session_no_close(event_loop): + session = aiohttp.ClientSession(loop=event_loop) + transport = asyncio.AsyncTransport(loop=event_loop, session=session) + del transport + assert not session.closed diff --git a/tests/test_client_factory.py b/tests/test_client_factory.py index 92d5b1a..632ae78 100644 --- a/tests/test_client_factory.py +++ b/tests/test_client_factory.py @@ -11,6 +11,18 @@ def test_factory_namespace(): assert obj.NameLast == 'van Tellingen' +def test_factory_no_reference(): + client = Client('tests/wsdl_files/soap.wsdl') + factory = client.type_factory('http://example.com/stockquote.xsd') + obj_1 = client.get_type('ns0:ArrayOfAddress')() + obj_1.Address.append({ + 'NameFirst': 'J', + 'NameLast': 'Doe', + }) + obj_2 = client.get_type('ns0:ArrayOfAddress')() + assert len(obj_2.Address) == 0 + + def test_factory_ns_auto_prefix(): client = Client('tests/wsdl_files/soap.wsdl') factory = client.type_factory('ns0') diff --git a/tests/test_loader.py b/tests/test_loader.py new file mode 100644 index 0000000..83ce9a7 --- /dev/null +++ b/tests/test_loader.py @@ -0,0 +1,14 @@ +from zeep.loader import parse_xml +from tests.utils import DummyTransport + +def test_huge_text(): + # libxml2>=2.7.3 has XML_MAX_TEXT_LENGTH 10000000 without XML_PARSE_HUGE + tree = parse_xml(u""" + + + %s + + + """ % (u'\u00e5' * 10000001), DummyTransport(), xml_huge_tree=True) + + assert tree[0][0].text == u'\u00e5' * 10000001 diff --git a/tests/test_soap_multiref.py b/tests/test_soap_multiref.py new file mode 100644 index 0000000..ae9ab7e --- /dev/null +++ b/tests/test_soap_multiref.py @@ -0,0 +1,134 @@ +import io + +import pytest +import requests_mock +from lxml import etree +from pretend import stub +from six import StringIO + +from tests.utils import DummyTransport, assert_nodes_equal +from zeep import Client, wsdl +from zeep.transports import Transport + + +@pytest.mark.requests +def test_parse_soap_wsdl(): + wsdl_file = io.StringIO(u""" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Test service + + + + + + """.strip()) + + content = """ + + + + + + + + + foo + bar + + bar + + + + + + foo + bar + + + + """.strip() + + client = Client(wsdl_file, transport=Transport(),) + response = stub( + status_code=200, + headers={}, + content=content) + + operation = client.service._binding._operations['TestOperation'] + result = client.service._binding.process_reply( + client, operation, response) + + assert result.item_1.subitem_1 == 'foo' + assert result.item_1.subitem_2 == 'bar' + assert result.item_2.subitem_1.subitem_1 == 'foo' + assert result.item_2.subitem_1.subitem_2 == 'bar' + assert result.item_2.subitem_2 == 'bar' diff --git a/tests/test_wsdl.py b/tests/test_wsdl.py index f42ba4c..2c34ac3 100644 --- a/tests/test_wsdl.py +++ b/tests/test_wsdl.py @@ -100,7 +100,8 @@ def test_parse_soap_header_wsdl(): } }) - assert result == 120.123 + assert result.body.price == 120.123 + assert result.header.body is None request = m.request_history[0] diff --git a/tests/test_wsdl_arrays.py b/tests/test_wsdl_arrays.py index 21b96d1..f64b7d2 100644 --- a/tests/test_wsdl_arrays.py +++ b/tests/test_wsdl_arrays.py @@ -1,12 +1,14 @@ import io +import pytest from lxml import etree -from tests.utils import DummyTransport, assert_nodes_equal, load_xml +from tests.utils import DummyTransport, assert_nodes_equal, load_xml, render_node from zeep import xsd -def get_transport(): +@pytest.fixture(scope='function') +def transport(): transport = DummyTransport() transport.bind( 'http://schemas.xmlsoap.org/soap/encoding/', @@ -14,7 +16,7 @@ def get_transport(): return transport -def test_simple_type(): +def test_simple_type(transport): schema = xsd.Schema(load_xml(""" - """), transport=get_transport()) + """), transport=transport) ArrayOfString = schema.get_type('ns0:ArrayOfString') print(ArrayOfString.__dict__) @@ -52,8 +54,105 @@ def test_simple_type(): assert_nodes_equal(expected, node) + data = ArrayOfString.parse_xmlelement(node, schema) + assert data == ['item', 'and', 'even', 'more', 'items'] + assert data.as_value_object() -def test_complex_type(): + +def test_simple_type_nested(transport): + schema = xsd.Schema(load_xml(""" + + + + + + + + + + + + + + + + + """), transport=transport) + + Container = schema.get_type('ns0:container') + value = Container(strings=['item', 'and', 'even', 'more', 'items']) + + assert value.strings == ['item', 'and', 'even', 'more', 'items'] + + node = etree.Element('document') + Container.render(node, value) + + expected = """ + + + item + and + even + more + items + + + """ # noqa + + assert_nodes_equal(expected, node) + + data = Container.parse_xmlelement(node, schema) + assert data.strings == ['item', 'and', 'even', 'more', 'items'] + + +def test_simple_type_nested_inline_type(transport): + schema = xsd.Schema(load_xml(""" + + + + + + + + + + + + + + + + + """), transport=transport) + + Container = schema.get_type('ns0:container') + node = load_xml(""" + + + item + and + even + more + items + + + """) # noqa + + data = Container.parse_xmlelement(node, schema) + assert data.strings == ['item', 'and', 'even', 'more', 'items'] + + +def test_complex_type(transport): schema = xsd.Schema(load_xml(""" - """), transport=get_transport()) + """), transport=transport) ArrayOfObject = schema.get_type('ns0:ArrayOfObject') ArrayObject = schema.get_type('ns0:ArrayObject') @@ -111,9 +210,17 @@ def test_complex_type(): """ assert_nodes_equal(expected, node) + data = ArrayOfObject.parse_xmlelement(node, schema) + + assert data[0].attr_1 == 'attr-1' + assert data[0].attr_2 == 'attr-2' + assert data[1].attr_1 == 'attr-3' + assert data[1].attr_2 == 'attr-4' + assert data[2].attr_1 == 'attr-5' + assert data[2].attr_2 == 'attr-6' -def test_complex_type_without_name(): +def test_complex_type_without_name(transport): schema = xsd.Schema(load_xml(""" - """), transport=get_transport()) + """), transport=transport) ArrayOfObject = schema.get_type('ns0:ArrayOfObject') ArrayObject = schema.get_type('ns0:ArrayObject') @@ -170,13 +277,13 @@ def test_complex_type_without_name(): assert_nodes_equal(expected, node) data = ArrayOfObject.parse_xmlelement(node, schema) - assert len(data._value_1) == 3 - assert data._value_1[0]['attr_1'] == 'attr-1' - assert data._value_1[0]['attr_2'] == 'attr-2' - assert data._value_1[1]['attr_1'] == 'attr-3' - assert data._value_1[1]['attr_2'] == 'attr-4' - assert data._value_1[2]['attr_1'] == 'attr-5' - assert data._value_1[2]['attr_2'] == 'attr-6' + assert len(data) == 3 + assert data[0]['attr_1'] == 'attr-1' + assert data[0]['attr_2'] == 'attr-2' + assert data[1]['attr_1'] == 'attr-3' + assert data[1]['attr_2'] == 'attr-4' + assert data[2]['attr_1'] == 'attr-5' + assert data[2]['attr_2'] == 'attr-6' def test_soap_array_parse_remote_ns(): @@ -237,8 +344,8 @@ def test_soap_array_parse_remote_ns(): elm = schema.get_element('ns0:countries') data = elm.parse(doc, schema) - assert data._value_1[0].code == 'NL' - assert data._value_1[0].name == 'The Netherlands' + assert data[0].code == 'NL' + assert data[0].name == 'The Netherlands' def test_wsdl_array_type(): @@ -284,9 +391,12 @@ def test_wsdl_array_type(): array = array_elm([item_1, item_2]) node = etree.Element('document') - assert array_elm.signature() == ( - '_value_1: base[], arrayType: xsd:string, offset: arrayCoordinate, ' + - 'id: xsd:ID, href: xsd:anyURI, _attr_1: {}') + assert array_elm.signature(schema=schema) == 'ns0:array(ns0:array)' + + array_type = schema.get_type('ns0:array') + assert array_type.signature(schema=schema) == ( + 'ns0:array(_value_1: base[], arrayType: xsd:string, ' + + 'offset: ns1:arrayCoordinate, id: xsd:ID, href: xsd:anyURI, _attr_1: {})') array_elm.render(node, array) expected = """ @@ -363,7 +473,80 @@ def test_soap_array_parse(): elm = schema.get_element('ns0:FlagDetailsList') data = elm.parse(doc, schema) - assert data.FlagDetailsStruct[0].Name == 'flag1' - assert data.FlagDetailsStruct[0].Value == 'value1' - assert data.FlagDetailsStruct[1].Name == 'flag2' - assert data.FlagDetailsStruct[1].Value == 'value2' + assert data[0].Name == 'flag1' + assert data[0].Value == 'value1' + assert data[1].Name == 'flag2' + assert data[1].Value == 'value2' + + +def test_xml_soap_enc_string(transport): + schema = xsd.Schema(load_xml(""" + + + + + + + + + + + + + + + + """), transport) + shoe_type = schema.get_element('{http://tests.python-zeep.org/}value') + + obj = shoe_type(["foo"]) + node = render_node(shoe_type, obj) + expected = """ + + + foo + + + """ + assert_nodes_equal(expected, node) + + obj = shoe_type.parse(node[0], schema) + assert obj[0]['_value_1'] == "foo" + + # Via string-types + string_type = schema.get_type('{http://schemas.xmlsoap.org/soap/encoding/}string') + obj = shoe_type([string_type('foo')]) + node = render_node(shoe_type, obj) + expected = """ + + + foo + + + """ + assert_nodes_equal(expected, node) + + obj = shoe_type.parse(node[0], schema) + assert obj[0]['_value_1'] == "foo" + + # Via dicts + string_type = schema.get_type('{http://schemas.xmlsoap.org/soap/encoding/}string') + obj = shoe_type([{'_value_1': 'foo'}]) + node = render_node(shoe_type, obj) + expected = """ + + + foo + + + """ + assert_nodes_equal(expected, node) + + obj = shoe_type.parse(node[0], schema) + assert obj[0]['_value_1'] == "foo" diff --git a/tests/test_wsdl_messages_document.py b/tests/test_wsdl_messages_document.py index c2a2389..151aef3 100644 --- a/tests/test_wsdl_messages_document.py +++ b/tests/test_wsdl_messages_document.py @@ -54,14 +54,14 @@ def test_parse(): binding = root.bindings['{http://tests.python-zeep.org/tns}TestBinding'] operation = binding.get('TestOperation') - assert operation.input.body.signature() == 'xsd:string' - assert operation.input.header.signature() == '' - assert operation.input.envelope.signature() == 'body: xsd:string' + assert operation.input.body.signature(schema=root.types) == 'ns0:Request(xsd:string)' + assert operation.input.header.signature(schema=root.types) == 'soap-env:Header()' + assert operation.input.envelope.signature(schema=root.types) == 'soap-env:envelope(body: xsd:string)' assert operation.input.signature(as_output=False) == 'xsd:string' - assert operation.output.body.signature() == 'xsd:string' - assert operation.output.header.signature() == '' - assert operation.output.envelope.signature() == 'body: xsd:string' + assert operation.output.body.signature(schema=root.types) == 'ns0:Response(xsd:string)' + assert operation.output.header.signature(schema=root.types) == 'soap-env:Header()' + assert operation.output.envelope.signature(schema=root.types) == 'soap-env:envelope(body: xsd:string)' assert operation.output.signature(as_output=True) == 'xsd:string' @@ -111,9 +111,9 @@ def test_empty_input_parse(): binding = root.bindings['{http://tests.python-zeep.org/tns}TestBinding'] operation = binding.get('TestOperation') - assert operation.input.body.signature() == '' - assert operation.input.header.signature() == '' - assert operation.input.envelope.signature() == 'body: {}' + assert operation.input.body.signature(schema=root.types) == 'soap-env:Body()' + assert operation.input.header.signature(schema=root.types) == 'soap-env:Header()' + assert operation.input.envelope.signature(schema=root.types) == 'soap-env:envelope(body: {})' assert operation.input.signature(as_output=False) == '' @@ -171,15 +171,15 @@ def test_parse_with_header(): binding = root.bindings['{http://tests.python-zeep.org/tns}TestBinding'] operation = binding.get('TestOperation') - assert operation.input.body.signature() == 'xsd:string' - assert operation.input.header.signature() == 'auth: RequestHeader()' - assert operation.input.envelope.signature() == 'body: xsd:string, header: {auth: RequestHeader()}' # noqa - assert operation.input.signature(as_output=False) == 'xsd:string, _soapheaders={auth: RequestHeader()}' # noqa + assert operation.input.body.signature(schema=root.types) == 'ns0:Request(xsd:string)' + assert operation.input.header.signature(schema=root.types) == 'soap-env:Header(auth: xsd:string)' + assert operation.input.envelope.signature(schema=root.types) == 'soap-env:envelope(header: {auth: xsd:string}, body: xsd:string)' # noqa + assert operation.input.signature(as_output=False) == 'xsd:string, _soapheaders={auth: xsd:string}' # noqa - assert operation.output.body.signature() == 'xsd:string' - assert operation.output.header.signature() == 'auth: ResponseHeader()' - assert operation.output.envelope.signature() == 'body: xsd:string, header: {auth: ResponseHeader()}' # noqa - assert operation.output.signature(as_output=True) == 'body: xsd:string, header: {auth: ResponseHeader()}' # noqa + assert operation.output.body.signature(schema=root.types) == 'ns0:Response(xsd:string)' + assert operation.output.header.signature(schema=root.types) == 'soap-env:Header(auth: xsd:string)' + assert operation.output.envelope.signature(schema=root.types) == 'soap-env:envelope(header: {auth: xsd:string}, body: xsd:string)' # noqa + assert operation.output.signature(as_output=True) == 'header: {auth: xsd:string}, body: xsd:string' # noqa def test_parse_with_header_type(): @@ -240,15 +240,15 @@ def test_parse_with_header_type(): binding = root.bindings['{http://tests.python-zeep.org/tns}TestBinding'] operation = binding.get('TestOperation') - assert operation.input.body.signature() == 'xsd:string' - assert operation.input.header.signature() == 'auth: RequestHeaderType' - assert operation.input.envelope.signature() == 'body: xsd:string, header: {auth: RequestHeaderType}' # noqa - assert operation.input.signature(as_output=False) == 'xsd:string, _soapheaders={auth: RequestHeaderType}' # noqa + assert operation.input.body.signature(schema=root.types) == 'ns0:Request(xsd:string)' + assert operation.input.header.signature(schema=root.types) == 'soap-env:Header(auth: ns0:RequestHeaderType)' + assert operation.input.envelope.signature(schema=root.types) == 'soap-env:envelope(header: {auth: ns0:RequestHeaderType}, body: xsd:string)' # noqa + assert operation.input.signature(as_output=False) == 'xsd:string, _soapheaders={auth: ns0:RequestHeaderType}' # noqa - assert operation.output.body.signature() == 'xsd:string' - assert operation.output.header.signature() == 'auth: ResponseHeaderType' - assert operation.output.envelope.signature() == 'body: xsd:string, header: {auth: ResponseHeaderType}' # noqa - assert operation.output.signature(as_output=True) == 'body: xsd:string, header: {auth: ResponseHeaderType}' # noqa + assert operation.output.body.signature(schema=root.types) == 'ns0:Response(xsd:string)' + assert operation.output.header.signature(schema=root.types) == 'soap-env:Header(auth: ns0:ResponseHeaderType)' + assert operation.output.envelope.signature(schema=root.types) == 'soap-env:envelope(header: {auth: ns0:ResponseHeaderType}, body: xsd:string)' # noqa + assert operation.output.signature(as_output=True) == 'header: {auth: ns0:ResponseHeaderType}, body: xsd:string' # noqa def test_parse_with_header_other_message(): @@ -292,12 +292,13 @@ def test_parse_with_header_other_message(): """.strip()) root = wsdl.Document(wsdl_content, None) + root.types.set_ns_prefix('soap-env', 'http://schemas.xmlsoap.org/soap/envelope/') binding = root.bindings['{http://tests.python-zeep.org/tns}TestBinding'] operation = binding.get('TestOperation') - assert operation.input.header.signature() == 'header: RequestHeader()' - assert operation.input.body.signature() == 'xsd:string' + assert operation.input.header.signature(schema=root.types) == 'soap-env:Header(header: xsd:string)' + assert operation.input.body.signature(schema=root.types) == 'ns0:Request(xsd:string)' header = root.types.get_element( '{http://tests.python-zeep.org/tns}RequestHeader' @@ -1193,7 +1194,7 @@ def test_deserialize_with_headers(): serialized = operation.process_reply(response_body) assert operation.output.signature(as_output=True) == ( - 'body: {request_1: Request1(), request_2: Request2()}, header: {header_1: Header1(), header_2: Header2()}') # noqa + 'header: {header_1: ns0:Header1, header_2: xsd:string}, body: {request_1: ns0:Request1, request_2: ns0:Request2}') assert serialized.body.request_1.arg1 == 'ah1' assert serialized.body.request_2.arg2 == 'ah2' assert serialized.header.header_1.username == 'mvantellingen' diff --git a/tests/test_wsdl_messages_http.py b/tests/test_wsdl_messages_http.py index aa40d25..eb48969 100644 --- a/tests/test_wsdl_messages_http.py +++ b/tests/test_wsdl_messages_http.py @@ -52,10 +52,10 @@ def test_urlencoded_serialize(): binding = root.bindings['{http://tests.python-zeep.org/tns}TestBinding'] operation = binding.get('TestOperation') - assert operation.input.body.signature() == 'arg1: xsd:string, arg2: xsd:string' + assert operation.input.body.signature(schema=root.types) == 'TestOperation(arg1: xsd:string, arg2: xsd:string)' assert operation.input.signature(as_output=False) == 'arg1: xsd:string, arg2: xsd:string' - assert operation.output.body.signature() == 'Body: xsd:string' + assert operation.output.body.signature(schema=root.types) == 'TestOperation(Body: xsd:string)' assert operation.output.signature(as_output=True) == 'xsd:string' serialized = operation.input.serialize(arg1='ah1', arg2='ah2') @@ -112,10 +112,10 @@ def test_urlreplacement_serialize(): binding = root.bindings['{http://tests.python-zeep.org/tns}TestBinding'] operation = binding.get('TestOperation') - assert operation.input.body.signature() == 'arg1: xsd:string, arg2: xsd:string' + assert operation.input.body.signature(schema=root.types) == 'TestOperation(arg1: xsd:string, arg2: xsd:string)' assert operation.input.signature(as_output=False) == 'arg1: xsd:string, arg2: xsd:string' - assert operation.output.body.signature() == 'Body: xsd:string' + assert operation.output.body.signature(schema=root.types) == 'TestOperation(Body: xsd:string)' assert operation.output.signature(as_output=True) == 'xsd:string' serialized = operation.input.serialize(arg1='ah1', arg2='ah2') @@ -172,10 +172,10 @@ def test_mime_content_serialize_form_urlencoded(): binding = root.bindings['{http://tests.python-zeep.org/tns}TestBinding'] operation = binding.get('TestOperation') - assert operation.input.body.signature() == 'arg1: xsd:string, arg2: xsd:string' + assert operation.input.body.signature(schema=root.types) == 'TestOperation(arg1: xsd:string, arg2: xsd:string)' assert operation.input.signature(as_output=False) == 'arg1: xsd:string, arg2: xsd:string' - assert operation.output.body.signature() == 'Body: xsd:string' + assert operation.output.body.signature(schema=root.types) == 'TestOperation(Body: xsd:string)' assert operation.output.signature(as_output=True) == 'xsd:string' serialized = operation.input.serialize(arg1='ah1', arg2='ah2') @@ -229,10 +229,10 @@ def test_mime_content_serialize_text_xml(): binding = root.bindings['{http://tests.python-zeep.org/tns}TestBinding'] operation = binding.get('TestOperation') - assert operation.input.body.signature() == 'arg1: xsd:string, arg2: xsd:string' + assert operation.input.body.signature(schema=root.types) == 'TestOperation(arg1: xsd:string, arg2: xsd:string)' assert operation.input.signature(as_output=False) == 'arg1: xsd:string, arg2: xsd:string' - assert operation.output.body.signature() == 'Body: xsd:string' + assert operation.output.body.signature(schema=root.types) == 'TestOperation(Body: xsd:string)' assert operation.output.signature(as_output=True) == 'xsd:string' serialized = operation.input.serialize(arg1='ah1', arg2='ah2') diff --git a/tests/test_wsdl_soap.py b/tests/test_wsdl_soap.py index 960645f..e80c152 100644 --- a/tests/test_wsdl_soap.py +++ b/tests/test_wsdl_soap.py @@ -1,12 +1,36 @@ +# -*- coding: utf-8 -*- + +import pytest + from lxml import etree from pretend import stub from tests.utils import load_xml from zeep import Client from zeep.exceptions import Fault +from zeep.exceptions import TransportError from zeep.wsdl import bindings +def test_soap11_no_output(): + client = Client('tests/wsdl_files/soap.wsdl') + content = """ + + + + """.strip() + response = stub( + status_code=200, + headers={}, + content=content) + + operation = client.service._binding._operations['GetLastTradePriceNoOutput'] + res = client.service._binding.process_reply(client, operation, response) + assert res is None + + def test_soap11_process_error(): response = load_xml(""" """) + binding = bindings.Soap11Binding( wsdl=None, name=None, port_name=None, transport=None, default_style=None) - try: binding.process_error(response, None) assert False @@ -112,6 +136,79 @@ def test_soap12_process_error(): assert exc.subcodes[1].localname == 'fault-subcode2' +def test_no_content_type(): + data = """ + + + + + + 120.123 + + + + """.strip() + + client = Client('tests/wsdl_files/soap.wsdl') + binding = client.service._binding + + response = stub( + status_code=200, + content=data, + encoding='utf-8', + headers={} + ) + + result = binding.process_reply( + client, binding.get('GetLastTradePrice'), response) + + assert result == 120.123 + + +def test_wrong_content(): + data = """ + The request is answered something unexpected, + like an html page or a raw internal stack trace + """.strip() + + client = Client('tests/wsdl_files/soap.wsdl') + binding = client.service._binding + + response = stub( + status_code=200, + content=data, + encoding='utf-8', + headers={} + ) + + with pytest.raises(TransportError): + binding.process_reply( + client, binding.get('GetLastTradePrice'), response) + + +def test_wrong_no_unicode_content(): + data = """ + The request is answered something unexpected, + and the content charset is beyond unicode òñÇÿ + """.strip() + + client = Client('tests/wsdl_files/soap.wsdl') + binding = client.service._binding + + response = stub( + status_code=200, + content=data, + encoding='utf-8', + headers={} + ) + + with pytest.raises(TransportError): + binding.process_reply( + client, binding.get('GetLastTradePrice'), response) + + def test_mime_multipart(): data = '\r\n'.join(line.strip() for line in """ --MIME_boundary @@ -154,6 +251,7 @@ def test_mime_multipart(): response = stub( status_code=200, content=data, + encoding='utf-8', headers={ 'Content-Type': 'multipart/related; type="text/xml"; start=""; boundary="MIME_boundary"' } @@ -167,3 +265,95 @@ def test_mime_multipart(): assert result.attachments[0].content == b'...Base64 encoded TIFF image...' assert result.attachments[1].content == b'...Raw JPEG image..' + + +def test_mime_multipart_no_encoding(): + data = '\r\n'.join(line.strip() for line in """ + --MIME_boundary + Content-Type: text/xml + Content-Transfer-Encoding: 8bit + Content-ID: + + + + + + + + + + + + + --MIME_boundary + Content-Type: image/tiff + Content-Transfer-Encoding: base64 + Content-ID: + + Li4uQmFzZTY0IGVuY29kZWQgVElGRiBpbWFnZS4uLg== + + --MIME_boundary + Content-Type: text/xml + Content-ID: + + ...Raw JPEG image.. + --MIME_boundary-- + """.splitlines()).encode('utf-8') + + client = Client('tests/wsdl_files/claim.wsdl') + binding = client.service._binding + + response = stub( + status_code=200, + content=data, + encoding=None, + headers={ + 'Content-Type': 'multipart/related; type="text/xml"; start=""; boundary="MIME_boundary"' + } + ) + + result = binding.process_reply( + client, binding.get('GetClaimDetails'), response) + + assert result.root is None + assert len(result.attachments) == 2 + + assert result.attachments[0].content == b'...Base64 encoded TIFF image...' + assert result.attachments[1].content == b'...Raw JPEG image..' + + +def test_unexpected_headers(): + data = """ + + + + uhoh + + + + 120.123 + + + + """.strip() + + client = Client('tests/wsdl_files/soap_header.wsdl') + binding = client.service._binding + + response = stub( + status_code=200, + content=data, + encoding='utf-8', + headers={} + ) + + result = binding.process_reply( + client, binding.get('GetLastTradePrice'), response) + + assert result.body.price == 120.123 + assert result.header.body is None + assert len(result.header._raw_elements) == 1 diff --git a/tests/test_xsd.py b/tests/test_xsd.py index 573784a..ebc99c6 100644 --- a/tests/test_xsd.py +++ b/tests/test_xsd.py @@ -149,39 +149,6 @@ def test_invalid_kwarg_simple_type(): elm(something='is-wrong') -def test_group_mixed(): - custom_type = xsd.Element( - etree.QName('http://tests.python-zeep.org/', 'authentication'), - xsd.ComplexType( - xsd.Sequence([ - xsd.Element( - etree.QName('http://tests.python-zeep.org/', 'username'), - xsd.String()), - xsd.Group( - etree.QName('http://tests.python-zeep.org/', 'groupie'), - xsd.Sequence([ - xsd.Element( - etree.QName('http://tests.python-zeep.org/', 'password'), - xsd.String(), - ) - ]) - ) - ]) - )) - assert custom_type.signature() - obj = custom_type(username='foo', password='bar') - - expected = """ - - - foo - bar - - - """ - node = etree.Element('document') - custom_type.render(node, obj) - assert_nodes_equal(expected, node) def test_any(): @@ -515,7 +482,7 @@ def test_duplicate_element_names(): )) # sequences - expected = 'item: xsd:string, item__1: xsd:string, item__2: xsd:string' + expected = '{http://tests.python-zeep.org/}container(item: xsd:string, item__1: xsd:string, item__2: xsd:string)' assert custom_type.signature() == expected obj = custom_type(item='foo', item__1='bar', item__2='lala') @@ -548,7 +515,7 @@ def test_element_attribute_name_conflict(): )) # sequences - expected = 'item: xsd:string, foo: xsd:string, attr__item: xsd:string' + expected = '{http://tests.python-zeep.org/}container(item: xsd:string, foo: xsd:string, attr__item: xsd:string)' assert custom_type.signature() == expected obj = custom_type(item='foo', foo='x', attr__item='bar') diff --git a/tests/test_xsd_any.py b/tests/test_xsd_any.py index 1595b2c..3d62b69 100644 --- a/tests/test_xsd_any.py +++ b/tests/test_xsd_any.py @@ -3,7 +3,7 @@ import datetime import pytest from lxml import etree -from tests.utils import assert_nodes_equal, load_xml +from tests.utils import assert_nodes_equal, load_xml, render_node from zeep import xsd @@ -26,6 +26,24 @@ def get_any_schema(): """)) + +def test_default_xsd_type(): + schema = xsd.Schema(load_xml(""" + + + + + """)) + assert schema + + container_cls = schema.get_element('ns0:container') + data = container_cls() + assert data == '' + + def test_any_simple(): schema = get_any_schema() @@ -109,6 +127,41 @@ def test_any_value_invalid(): container_elm.render(node, obj) +def test_any_without_element(): + schema = xsd.Schema(load_xml(""" + + + + + + + + + + + + + """)) + item_elm = schema.get_element('{http://tests.python-zeep.org/}item') + item = item_elm(xsd.AnyObject(xsd.String(), 'foobar'), type='attr-1', title='attr-2') + + node = render_node(item_elm, item) + expected = """ + + foobar + + """ + assert_nodes_equal(expected, node) + + item = item_elm.parse(node.getchildren()[0], schema) + assert item.type == 'attr-1' + assert item.title == 'attr-2' + assert item._value_1 is None + + def test_any_with_ref(): schema = xsd.Schema(load_xml(""" @@ -219,6 +272,36 @@ def test_element_any_type(): item = container_elm.parse(node.getchildren()[0], schema) assert item.something == 'bar' +def test_element_any_type_unknown_type(): + node = etree.fromstring(""" + + + + + + + + + + + """.strip()) + schema = xsd.Schema(node) + + container_elm = schema.get_element('{http://tests.python-zeep.org/}container') + + node = load_xml(""" + + + bar + + + """) + item = container_elm.parse(node.getchildren()[0], schema) + assert item.something == 'bar' + def test_element_any_type_elements(): node = etree.fromstring(""" @@ -298,8 +381,8 @@ def test_any_in_nested_sequence(): """)) # noqa container_elm = schema.get_element('{http://tests.python-zeep.org/}container') - assert container_elm.signature() == ( - 'items: {_value_1: ANY}, version: xsd:string, _value_1: ANY[]') + assert container_elm.signature(schema) == ( + 'ns0:container(items: {_value_1: ANY}, version: xsd:string, _value_1: ANY[])') something = schema.get_element('{http://tests.python-zeep.org/}something') foobar = schema.get_element('{http://tests.python-zeep.org/}foobar') diff --git a/tests/test_xsd_attributes.py b/tests/test_xsd_attributes.py index f30d76e..535c94a 100644 --- a/tests/test_xsd_attributes.py +++ b/tests/test_xsd_attributes.py @@ -26,8 +26,8 @@ def test_anyattribute(): """)) container_elm = schema.get_element('{http://tests.python-zeep.org/}container') - assert container_elm.signature() == ( - 'foo: xsd:string, _attr_1: {}') + assert container_elm.signature(schema) == ( + 'ns0:container(foo: xsd:string, _attr_1: {})') obj = container_elm(foo='bar', _attr_1=OrderedDict([ ('hiep', 'hoi'), ('hoi', 'hiep') ])) @@ -73,7 +73,8 @@ def test_attribute_list_type(): """)) container_elm = schema.get_element('{http://tests.python-zeep.org/}container') - assert container_elm.signature() == ('foo: xsd:string, lijst: xsd:int[]') + assert container_elm.signature(schema) == ( + 'ns0:container(foo: xsd:string, lijst: xsd:int[])') obj = container_elm(foo='bar', lijst=[1, 2, 3]) expected = """ @@ -341,7 +342,8 @@ def test_nested_attribute(): """)) container_elm = schema.get_element('{http://tests.python-zeep.org/}container') - assert container_elm.signature() == 'item: {x: xsd:string, y: xsd:string}' + assert container_elm.signature(schema) == ( + 'ns0:container(item: {x: xsd:string, y: xsd:string})') obj = container_elm(item={'x': 'foo', 'y': 'bar'}) expected = """ @@ -371,7 +373,7 @@ def test_attribute_union_type(): - + diff --git a/tests/test_xsd_builtins.py b/tests/test_xsd_builtins.py index abbab75..17ae68e 100644 --- a/tests/test_xsd_builtins.py +++ b/tests/test_xsd_builtins.py @@ -30,6 +30,8 @@ class TestBoolean: assert instance.xmlvalue(False) == 'false' assert instance.xmlvalue(1) == 'true' assert instance.xmlvalue(0) == 'false' + assert instance.xmlvalue('false') == 'false' + assert instance.xmlvalue('0') == 'false' def test_pythonvalue(self): instance = builtins.Boolean() diff --git a/tests/test_xsd_complex_types.py b/tests/test_xsd_complex_types.py index c22f185..a6d862e 100644 --- a/tests/test_xsd_complex_types.py +++ b/tests/test_xsd_complex_types.py @@ -1,11 +1,11 @@ -import pytest from lxml import etree +import pytest from tests.utils import assert_nodes_equal, load_xml, render_node from zeep import xsd -def test_single_node(): +def test_xml_xml_single_node(): schema = xsd.Schema(load_xml(""" + + + + + + + + + + + + + + + + + + + """)) + schema.set_ns_prefix('tns', 'http://tests.python-zeep.org/') + container_elm = schema.get_element('tns:container') + container_elm.signature(schema) + + +def test_xml_single_node_array(): schema = xsd.Schema(load_xml(""" @@ -231,3 +261,37 @@ def test_complex_any_types(): """) # noqa assert_nodes_equal(result, expected) + + +def test_xml_unparsed_elements(): + schema = xsd.Schema(load_xml(""" + + + + + + + + + + + """)) + schema.strict = False + schema.set_ns_prefix('tns', 'http://tests.python-zeep.org/') + + expected = load_xml(""" + + + bar + bar + + + """) + + container_elm = schema.get_element('tns:container') + obj = container_elm.parse(expected[0], schema) + assert obj.item == 'bar' + assert obj._raw_elements diff --git a/tests/test_xsd_extension.py b/tests/test_xsd_extension.py index 206b5f0..b80faf8 100644 --- a/tests/test_xsd_extension.py +++ b/tests/test_xsd_extension.py @@ -123,7 +123,8 @@ def test_complex_content_with_recursive_elements(): """)) pet_type = schema.get_element('{http://tests.python-zeep.org/}Pet') - assert(pet_type.signature() == 'name: xsd:string, common_name: xsd:string, children: Pet') + assert(pet_type.signature(schema=schema) == 'ns0:Pet(ns0:Pet)') + assert(pet_type.type.signature(schema=schema) == 'ns0:Pet(name: xsd:string, common_name: xsd:string, children: ns0:Pet[])') obj = pet_type( name='foo', common_name='bar', @@ -602,3 +603,100 @@ def test_extension_abstract_complex_type(): """ assert_nodes_equal(expected, node) + + +def test_extension_base_anytype(): + schema = xsd.Schema(load_xml(""" + + + + + + + + + + + + + + + """)) + container_elm = schema.get_element('{http://tests.python-zeep.org/}container') + + assert container_elm.signature() == ( + '{http://tests.python-zeep.org/}container(attr: xsd:unsignedInt, _attr_1: {})') + + obj = container_elm(attr='foo') + + node = render_node(container_elm, obj) + expected = """ + + + + """ + assert_nodes_equal(expected, node) + + +def test_extension_on_ref(): + schema = xsd.Schema(load_xml(""" + + + + + + + + + + + + + + + + + + + """)) + + type_cls = schema.get_type('ns0:type') + assert type_cls.signature() + + +def test_restrict_on_ref(): + schema = xsd.Schema(load_xml(""" + + + + + + + + + + + + + + + + + + + """)) + + type_cls = schema.get_type('ns0:type') + assert type_cls.signature() diff --git a/tests/test_xsd_indicators_all.py b/tests/test_xsd_indicators_all.py new file mode 100644 index 0000000..73b9939 --- /dev/null +++ b/tests/test_xsd_indicators_all.py @@ -0,0 +1,65 @@ +from lxml import etree + +from tests.utils import assert_nodes_equal, render_node, load_xml +from zeep import xsd + + +def test_build_occurs_1(): + custom_type = xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'authentication'), + xsd.ComplexType( + xsd.All([ + xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'item_1'), + xsd.String()), + xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'item_2'), + xsd.String()), + ]) + )) + + obj = custom_type(item_1='foo', item_2='bar') + result = render_node(custom_type, obj) + + expected = load_xml(""" + + + foo + bar + + + """) + + assert_nodes_equal(result, expected) + + obj = custom_type.parse(result[0], None) + assert obj.item_1 == 'foo' + assert obj.item_2 == 'bar' + + +def test_build_pare_other_order(): + custom_type = xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'authentication'), + xsd.ComplexType( + xsd.All([ + xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'item_1'), + xsd.String()), + xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'item_2'), + xsd.String()), + ]) + )) + + xml = load_xml(""" + + + bar + foo + + + """) + + obj = custom_type.parse(xml[0], None) + assert obj.item_1 == 'foo' + assert obj.item_2 == 'bar' diff --git a/tests/test_xsd_choice.py b/tests/test_xsd_indicators_choice.py similarity index 73% rename from tests/test_xsd_choice.py rename to tests/test_xsd_indicators_choice.py index cff9296..00b00fb 100644 --- a/tests/test_xsd_choice.py +++ b/tests/test_xsd_indicators_choice.py @@ -1,9 +1,10 @@ +from collections import deque import pytest from lxml import etree from tests.utils import assert_nodes_equal, load_xml, render_node from zeep import xsd -from zeep.exceptions import XMLParseError +from zeep.exceptions import XMLParseError, ValidationError from zeep.helpers import serialize_object @@ -45,7 +46,7 @@ def test_choice_element(): element.render(node, value) assert_nodes_equal(expected, node) - value = element.parse(node.getchildren()[0], schema) + value = element.parse(node[0], schema) assert value.item_1 == 'foo' assert value.item_2 is None assert value.item_3 is None @@ -89,11 +90,89 @@ def test_choice_element_second_elm(): element.render(node, value) assert_nodes_equal(expected, node) - value = element.parse(node.getchildren()[0], schema) + value = element.parse(node[0], schema) assert value.item_1 is None assert value.item_2 == 'foo' assert value.item_3 is None +def test_choice_element_second_elm_positional(): + node = etree.fromstring(""" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + """.strip()) + schema = xsd.Schema(node) + + child = schema.get_type('ns0:type_2')(child_1='ha', child_2='ho') + + element = schema.get_element('ns0:container') + with pytest.raises(TypeError): + value = element(child) + value = element(item_2=child) + + element = schema.get_element('ns0:containerArray') + with pytest.raises(TypeError): + value = element(child) + value = element(item_2=child) + + element = schema.get_element('ns0:container') + value = element(item_2=child) + assert value.item_1 is None + assert value.item_2 == child + + expected = """ + + + + ha + ho + + + + """ + node = etree.Element('document') + element.render(node, value) + assert_nodes_equal(expected, node) + + value = element.parse(node[0], schema) + assert value.item_1 is None + assert value.item_2.child_1 == 'ha' + assert value.item_2.child_2 == 'ho' + def test_choice_element_multiple(): node = etree.fromstring(""" @@ -137,7 +216,7 @@ def test_choice_element_multiple(): element.render(node, value) assert_nodes_equal(expected, node) - value = element.parse(node.getchildren()[0], schema) + value = element.parse(node[0], schema) assert value._value_1 == [ {'item_1': 'foo'}, {'item_2': 'bar'}, {'item_1': 'three'}, ] @@ -179,6 +258,8 @@ def test_choice_element_optional(): node = etree.Element('document') element.render(node, value) assert_nodes_equal(expected, node) + value = element.parse(node[0], schema) + assert value.item_4 == 'foo' def test_choice_element_with_any(): @@ -219,7 +300,7 @@ def test_choice_element_with_any(): element.render(node, value) assert_nodes_equal(expected, node) - result = element.parse(node.getchildren()[0], schema) + result = element.parse(node[0], schema) assert result.name == 'foo' assert result.something is True assert result.item_1 == 'foo' @@ -266,7 +347,7 @@ def test_choice_element_with_any_max_occurs(): """ node = render_node(element, value) assert_nodes_equal(node, expected) - result = element.parse(node.getchildren()[0], schema) + result = element.parse(node[0], schema) assert result.item_2 == 'item-2' assert result._value_1 == ['any-content'] @@ -319,8 +400,8 @@ def test_choice_in_sequence(): schema = xsd.Schema(node) container_elm = schema.get_element('ns0:container') - assert container_elm.type.signature() == ( - 'something: xsd:string, ({item_1: xsd:string} | {item_2: xsd:string} | {item_3: xsd:string})') # noqa + assert container_elm.type.signature(schema=schema) == ( + 'ns0:container(something: xsd:string, ({item_1: xsd:string} | {item_2: xsd:string} | {item_3: xsd:string}))') value = container_elm(something='foobar', item_1='item-1') expected = """ @@ -334,6 +415,7 @@ def test_choice_in_sequence(): node = etree.Element('document') container_elm.render(node, value) assert_nodes_equal(expected, node) + value = container_elm.parse(node[0], schema) def test_choice_with_sequence(): @@ -362,8 +444,8 @@ def test_choice_with_sequence(): """) schema = xsd.Schema(node) element = schema.get_element('ns0:container') - assert element.type.signature() == ( - '({item_1: xsd:string, item_2: xsd:string} | {item_3: xsd:string, item_4: xsd:string})') + assert element.type.signature(schema=schema) == ( + 'ns0:container(({item_1: xsd:string, item_2: xsd:string} | {item_3: xsd:string, item_4: xsd:string}))') value = element(item_1='foo', item_2='bar') expected = """ @@ -377,6 +459,7 @@ def test_choice_with_sequence(): node = etree.Element('document') element.render(node, value) assert_nodes_equal(expected, node) + value = element.parse(node[0], schema) def test_choice_with_sequence_once(): @@ -404,8 +487,8 @@ def test_choice_with_sequence_once(): """) schema = xsd.Schema(node) element = schema.get_element('ns0:container') - assert element.type.signature() == ( - 'item_0: xsd:string, ({item_1: xsd:string, item_2: xsd:string})') + assert element.type.signature(schema=schema) == ( + 'ns0:container(item_0: xsd:string, ({item_1: xsd:string, item_2: xsd:string}))') value = element(item_0='nul', item_1='foo', item_2='bar') expected = """ @@ -420,6 +503,94 @@ def test_choice_with_sequence_once(): node = etree.Element('document') element.render(node, value) assert_nodes_equal(expected, node) + value = element.parse(node[0], schema) + + +def test_choice_with_sequence_unbounded(): + node = load_xml(""" + + + + + + + + + + + + + + + + + + + + """) + schema = xsd.Schema(node) + element = schema.get_element('ns0:container') + assert element.type.signature(schema=schema) == ( + 'ns0:container(({[item_0: xsd:string, item_1: xsd:string, item_2: ns0:obj]}))') + value = element(_value_1=[ + {'item_0': 'nul', 'item_1': 'foo', 'item_2': {'_value_1': [{'item_2_1': 'bar'}]}}, + ]) + + expected = """ + + + nul + foo + + bar + + + + """ + node = etree.Element('document') + element.render(node, value) + assert_nodes_equal(expected, node) + + value = element.parse(node[0], schema) + assert value._value_1[0]['item_0'] == 'nul' + assert value._value_1[0]['item_1'] == 'foo' + assert value._value_1[0]['item_2']._value_1[0]['item_2_1'] == 'bar' + + assert not hasattr(value._value_1[0]['item_2'], 'item_2_1') + + +def test_choice_with_sequence_missing_elements(): + node = load_xml(""" + + + + + + + + + + + + + + """) + schema = xsd.Schema(node) + element = schema.get_element('ns0:container') + assert element.type.signature(schema=schema) == ( + 'ns0:container(({item_1: xsd:string, item_2: xsd:string})[])') + + value = element(_value_1={'item_1': 'foo'}) + with pytest.raises(ValidationError): + render_node(element, value) def test_choice_with_sequence_once_extra_data(): @@ -448,8 +619,8 @@ def test_choice_with_sequence_once_extra_data(): """) schema = xsd.Schema(node) element = schema.get_element('ns0:container') - assert element.type.signature() == ( - 'item_0: xsd:string, ({item_1: xsd:string, item_2: xsd:string}), item_3: xsd:string') + assert element.type.signature(schema=schema) == ( + 'ns0:container(item_0: xsd:string, ({item_1: xsd:string, item_2: xsd:string}), item_3: xsd:string)') value = element(item_0='nul', item_1='foo', item_2='bar', item_3='item-3') expected = """ @@ -465,6 +636,7 @@ def test_choice_with_sequence_once_extra_data(): node = etree.Element('document') element.render(node, value) assert_nodes_equal(expected, node) + value = element.parse(node[0], schema) def test_choice_with_sequence_second(): @@ -493,8 +665,8 @@ def test_choice_with_sequence_second(): """) schema = xsd.Schema(node) element = schema.get_element('ns0:container') - assert element.type.signature() == ( - '({item_1: xsd:string, item_2: xsd:string} | {item_3: xsd:string, item_4: xsd:string})') + assert element.type.signature(schema=schema) == ( + 'ns0:container(({item_1: xsd:string, item_2: xsd:string} | {item_3: xsd:string, item_4: xsd:string}))') value = element(item_3='foo', item_4='bar') expected = """ @@ -508,6 +680,7 @@ def test_choice_with_sequence_second(): node = etree.Element('document') element.render(node, value) assert_nodes_equal(expected, node) + value = element.parse(node[0], schema) def test_choice_with_sequence_invalid(): @@ -536,8 +709,8 @@ def test_choice_with_sequence_invalid(): """) schema = xsd.Schema(node) element = schema.get_element('ns0:container') - assert element.type.signature() == ( - '({item_1: xsd:string, item_2: xsd:string} | {item_3: xsd:string, item_4: xsd:string})') + assert element.type.signature(schema=schema) == ( + 'ns0:container(({item_1: xsd:string, item_2: xsd:string} | {item_3: xsd:string, item_4: xsd:string}))') with pytest.raises(TypeError): element(item_1='foo', item_4='bar') @@ -594,6 +767,7 @@ def test_choice_with_sequence_change(): node = etree.Element('document') element.render(node, elm) assert_nodes_equal(expected, node) + value = element.parse(node[0], schema) def test_choice_with_sequence_change_named(): @@ -638,6 +812,7 @@ def test_choice_with_sequence_change_named(): node = etree.Element('document') element.render(node, elm) assert_nodes_equal(expected, node) + value = element.parse(node[0], schema) def test_choice_with_sequence_multiple(): @@ -666,8 +841,8 @@ def test_choice_with_sequence_multiple(): """) schema = xsd.Schema(node) element = schema.get_element('ns0:container') - assert element.type.signature() == ( - '({item_1: xsd:string, item_2: xsd:string} | {item_3: xsd:string, item_4: xsd:string})[]') + assert element.type.signature(schema=schema) == ( + 'ns0:container(({item_1: xsd:string, item_2: xsd:string} | {item_3: xsd:string, item_4: xsd:string})[])') value = element(_value_1=[ dict(item_1='foo', item_2='bar'), dict(item_3='foo', item_4='bar'), @@ -686,6 +861,7 @@ def test_choice_with_sequence_multiple(): node = etree.Element('document') element.render(node, value) assert_nodes_equal(expected, node) + value = element.parse(node[0], schema) def test_choice_with_sequence_and_element(): @@ -713,8 +889,8 @@ def test_choice_with_sequence_and_element(): """) schema = xsd.Schema(node) element = schema.get_element('ns0:container') - assert element.type.signature() == ( - '({item_1: xsd:string} | {({item_2: xsd:string} | {item_3: xsd:string})})') + assert element.type.signature(schema=schema) == ( + 'ns0:container(({item_1: xsd:string} | {({item_2: xsd:string} | {item_3: xsd:string})}))') value = element(item_2='foo') @@ -728,6 +904,7 @@ def test_choice_with_sequence_and_element(): node = etree.Element('document') element.render(node, value) assert_nodes_equal(expected, node) + value = element.parse(node[0], schema) def test_element_ref_in_choice(): @@ -1043,3 +1220,138 @@ def test_choice_extend(): assert value['item-1-2'] == 'bar' assert value['_value_1'][0] == {'item-2-1': 'xafoo'} assert value['_value_1'][1] == {'item-2-2': 'xabar'} + + +def test_nested_choice(): + schema = xsd.Schema(load_xml(""" + + + + + + + + + + + + + + + + + + """)) + + schema.set_ns_prefix('tns', 'http://tests.python-zeep.org/') + container_type = schema.get_element('tns:container') + + item = container_type(_value_1=[{'a': 'item-1'}, {'a': 'item-2'}]) + assert item._value_1[0] == {'a': 'item-1'} + assert item._value_1[1] == {'a': 'item-2'} + + expected = load_xml(""" + + + item-1 + item-2 + + + """) + node = render_node(container_type, item) + assert_nodes_equal(node, expected) + + result = container_type.parse(expected[0], schema) + assert result._value_1[0] == {'a': 'item-1'} + assert result._value_1[1] == {'a': 'item-2'} + + expected = load_xml(""" + + 1 + + """) + + result = container_type.parse(expected, schema) + assert result.b == '1' + + +def test_unit_choice_parse_xmlelements_max_1(): + schema = xsd.Schema(load_xml(""" + + + + + + + + + + + + + """)) + element = schema.get_element('ns0:container') + + def create_elm(name, text): + elm = etree.Element(name) + elm.text = text + return elm + + data = deque([ + create_elm('item_1', 'item-1'), + create_elm('item_2', 'item-2'), + create_elm('item_1', 'item-3'), + ]) + + result = element.type._element.parse_xmlelements(data, schema) + assert result == {'item_1': 'item-1'} + assert len(data) == 2 + + +def test_unit_choice_parse_xmlelements_max_2(): + schema = xsd.Schema(load_xml(""" + + + + + + + + + + + + + """)) + element = schema.get_element('ns0:container') + + def create_elm(name, text): + elm = etree.Element(name) + elm.text = text + return elm + + data = deque([ + create_elm('item_1', 'item-1'), + create_elm('item_2', 'item-2'), + create_elm('item_1', 'item-3'), + ]) + + result = element.type._element.parse_xmlelements(data, schema, name='items') + assert result == { + 'items': [ + {'item_1': 'item-1'}, + {'item_2': 'item-2'}, + ] + } + assert len(data) == 1 diff --git a/tests/test_xsd_indicators_group.py b/tests/test_xsd_indicators_group.py new file mode 100644 index 0000000..68d5924 --- /dev/null +++ b/tests/test_xsd_indicators_group.py @@ -0,0 +1,417 @@ +import pytest +from lxml import etree + +from tests.utils import assert_nodes_equal, render_node, load_xml +from zeep import xsd + + +def test_build_objects(): + custom_type = xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'authentication'), + xsd.ComplexType( + xsd.Sequence([ + xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'username'), + xsd.String()), + xsd.Group( + etree.QName('http://tests.python-zeep.org/', 'groupie'), + xsd.Sequence([ + xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'password'), + xsd.String(), + ) + ]) + ) + ]) + )) + assert custom_type.signature() + obj = custom_type(username='foo', password='bar') + + expected = """ + + + foo + bar + + + """ + node = etree.Element('document') + custom_type.render(node, obj) + assert_nodes_equal(expected, node) + + obj = custom_type.parse(node[0], None) + assert obj.username == 'foo' + assert obj.password == 'bar' + + +def test_build_group_min_occurs_1(): + custom_type = xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'authentication'), + xsd.ComplexType( + xsd.Group( + etree.QName('http://tests.python-zeep.org/', 'foobar'), + xsd.Sequence([ + xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'item_1'), + xsd.String()), + xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'item_2'), + xsd.String()), + ]), + min_occurs=1) + )) + + obj = custom_type(item_1='foo', item_2='bar') + assert obj.item_1 == 'foo' + assert obj.item_2 == 'bar' + + result = render_node(custom_type, obj) + expected = load_xml(""" + + + foo + bar + + + """) + + assert_nodes_equal(result, expected) + + obj = custom_type.parse(result[0], None) + assert obj.item_1 == 'foo' + assert obj.item_2 == 'bar' + assert not hasattr(obj, 'foobar') + + +def test_build_group_min_occurs_1_parse_args(): + custom_type = xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'authentication'), + xsd.ComplexType( + xsd.Group( + etree.QName('http://tests.python-zeep.org/', 'foobar'), + xsd.Sequence([ + xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'item_1'), + xsd.String()), + xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'item_2'), + xsd.String()), + ]), + min_occurs=1) + )) + + obj = custom_type('foo', 'bar') + assert obj.item_1 == 'foo' + assert obj.item_2 == 'bar' + + +def test_build_group_min_occurs_2(): + custom_type = xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'authentication'), + xsd.ComplexType( + xsd.Group( + etree.QName('http://tests.python-zeep.org/', 'foobar'), + xsd.Sequence([ + xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'item_1'), + xsd.String()), + xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'item_2'), + xsd.String()), + ]), + min_occurs=2, max_occurs=2) + )) + + obj = custom_type(_value_1=[ + {'item_1': 'foo', 'item_2': 'bar'}, + {'item_1': 'foo', 'item_2': 'bar'}, + ]) + assert obj._value_1 == [ + {'item_1': 'foo', 'item_2': 'bar'}, + {'item_1': 'foo', 'item_2': 'bar'}, + ] + + result = render_node(custom_type, obj) + expected = load_xml(""" + + + foo + bar + foo + bar + + + """) + + assert_nodes_equal(result, expected) + + obj = custom_type.parse(result[0], None) + assert obj._value_1 == [ + {'item_1': 'foo', 'item_2': 'bar'}, + {'item_1': 'foo', 'item_2': 'bar'}, + ] + assert not hasattr(obj, 'foobar') + + +def test_build_group_min_occurs_2_sequence_min_occurs_2(): + custom_type = xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'authentication'), + xsd.ComplexType( + xsd.Group( + etree.QName('http://tests.python-zeep.org/', 'foobar'), + xsd.Sequence([ + xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'item_1'), + xsd.String()), + xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'item_2'), + xsd.String()), + ], min_occurs=2, max_occurs=2), + min_occurs=2, max_occurs=2) + )) + expected = etree.fromstring(""" + + foo + bar + foo + bar + foo + bar + foo + bar + + """) + obj = custom_type.parse(expected, None) + assert obj._value_1 == [ + {'_value_1': [ + {'item_1': 'foo', 'item_2': 'bar'}, + {'item_1': 'foo', 'item_2': 'bar'}, + ]}, + {'_value_1': [ + {'item_1': 'foo', 'item_2': 'bar'}, + {'item_1': 'foo', 'item_2': 'bar'}, + ]}, + ] + assert not hasattr(obj, 'foobar') + + +def test_build_group_occurs_1_invalid_kwarg(): + custom_type = xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'authentication'), + xsd.ComplexType( + xsd.Group( + etree.QName('http://tests.python-zeep.org/', 'foobar'), + xsd.Sequence([ + xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'item_1'), + xsd.String()), + xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'item_2'), + xsd.String()), + ]), + min_occurs=1, max_occurs=1) + )) + + with pytest.raises(TypeError): + custom_type(item_1='foo', item_2='bar', error=True) + + +def test_build_group_min_occurs_2_invalid_kwarg(): + custom_type = xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'authentication'), + xsd.ComplexType( + xsd.Group( + etree.QName('http://tests.python-zeep.org/', 'foobar'), + xsd.Sequence([ + xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'item_1'), + xsd.String()), + xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'item_2'), + xsd.String()), + ]), + min_occurs=2, max_occurs=2) + )) + + with pytest.raises(TypeError): + custom_type(_value_1=[ + {'item_1': 'foo', 'item_2': 'bar', 'error': True}, + {'item_1': 'foo', 'item_2': 'bar'}, + ]) + + +def test_xml_group_via_ref(): + schema = xsd.Schema(load_xml(""" + + + + + + + + + + + + + + + + + + """)) + address_type = schema.get_element('{http://tests.python-zeep.org/}Address') + + obj = address_type(first_name='foo', last_name='bar') + + node = etree.Element('document') + address_type.render(node, obj) + expected = """ + + + foo + bar + + + """ + assert_nodes_equal(expected, node) + + +def test_xml_group_via_ref_max_occurs_unbounded(): + schema = xsd.Schema(load_xml(""" + + + + + + + + + + + + + + + + + + """)) + address_type = schema.get_element('{http://tests.python-zeep.org/}Address') + + obj = address_type( + _value_1=[ + {'first_name': 'foo-1', 'last_name': 'bar-1'}, + {'first_name': 'foo-2', 'last_name': 'bar-2'}, + ]) + + node = etree.Element('document') + address_type.render(node, obj) + expected = """ + + + foo-1 + bar-1 + foo-2 + bar-2 + + + """ + assert_nodes_equal(expected, node) + + obj = address_type.parse(node[0], None) + assert obj._value_1[0]['first_name'] == 'foo-1' + assert obj._value_1[0]['last_name'] == 'bar-1' + assert obj._value_1[1]['first_name'] == 'foo-2' + assert obj._value_1[1]['last_name'] == 'bar-2' + + +def test_xml_multiple_groups_in_sequence(): + schema = xsd.Schema(load_xml(""" + + + + + + + + + + + + + + + + + + + + + + blub + + + + + + + + """)) + address_type = schema.get_element('{http://tests.python-zeep.org/}Address') + + obj = address_type( + first_name='foo', last_name='bar', + city='Utrecht', country='The Netherlands') + + node = etree.Element('document') + address_type.render(node, obj) + expected = """ + + + foo + bar + Utrecht + The Netherlands + + + """ + assert_nodes_equal(expected, node) + + +def test_xml_group_methods(): + schema = xsd.Schema(load_xml(""" + + + + + + blub + + + + + + + + + """)) + Group = schema.get_group('{http://tests.python-zeep.org/}Group') + assert Group.signature(schema) == ( + 'ns0:Group(city: xsd:string, country: xsd:string)') + assert str(Group) == ( + '{http://tests.python-zeep.org/}Group(city: xsd:string, country: xsd:string)') + + assert len(list(Group)) == 2 diff --git a/tests/test_xsd_indicators_sequence.py b/tests/test_xsd_indicators_sequence.py new file mode 100644 index 0000000..63b7c7b --- /dev/null +++ b/tests/test_xsd_indicators_sequence.py @@ -0,0 +1,477 @@ +import pytest +from lxml import etree + +from tests.utils import load_xml, render_node, assert_nodes_equal +from zeep import xsd + + +def test_build_occurs_1(): + custom_type = xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'authentication'), + xsd.ComplexType( + xsd.Sequence([ + xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'item_1'), + xsd.String()), + xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'item_2'), + xsd.String()), + ]) + )) + obj = custom_type(item_1='foo', item_2='bar') + assert obj.item_1 == 'foo' + assert obj.item_2 == 'bar' + + result = render_node(custom_type, obj) + expected = load_xml(""" + + + foo + bar + + + """) + assert_nodes_equal(result, expected) + obj = custom_type.parse(expected[0], None) + assert obj.item_1 == 'foo' + assert obj.item_2 == 'bar' + + +def test_build_occurs_1_skip_value(): + custom_type = xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'authentication'), + xsd.ComplexType( + xsd.Sequence([ + xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'item_1'), + xsd.String()), + xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'item_2'), + xsd.String()), + ]) + )) + obj = custom_type(item_1=xsd.SkipValue, item_2='bar') + assert obj.item_1 == xsd.SkipValue + assert obj.item_2 == 'bar' + + result = render_node(custom_type, obj) + expected = load_xml(""" + + + bar + + + """) + assert_nodes_equal(result, expected) + + +def test_build_min_occurs_2_max_occurs_2(): + custom_type = xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'authentication'), + xsd.ComplexType( + xsd.Sequence([ + xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'item_1'), + xsd.String()), + xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'item_2'), + xsd.String()), + ], min_occurs=2, max_occurs=2) + )) + + assert custom_type.signature() + + + elm = custom_type(_value_1=[ + {'item_1': 'foo-1', 'item_2': 'bar-1'}, + {'item_1': 'foo-2', 'item_2': 'bar-2'}, + ]) + + assert elm._value_1 == [ + {'item_1': 'foo-1', 'item_2': 'bar-1'}, + {'item_1': 'foo-2', 'item_2': 'bar-2'}, + ] + + expected = load_xml(""" + + foo + bar + foo + bar + + """) + obj = custom_type.parse(expected, None) + assert obj._value_1 == [ + { + 'item_1': 'foo', + 'item_2': 'bar', + }, + { + 'item_1': 'foo', + 'item_2': 'bar', + }, + ] + + +def test_build_min_occurs_2_max_occurs_2_error(): + custom_type = xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'authentication'), + xsd.ComplexType( + xsd.Sequence([ + xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'item_1'), + xsd.String()), + xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'item_2'), + xsd.String()), + ], min_occurs=2, max_occurs=2) + )) + + with pytest.raises(TypeError): + custom_type(_value_1={ + 'item_1': 'foo-1', 'item_2': 'bar-1', 'error': True + }) + + +def test_build_sequence_and_attributes(): + custom_element = xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'authentication'), + xsd.ComplexType( + xsd.Sequence([ + xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'item_1'), + xsd.String()), + xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'item_2'), + xsd.String()), + ]), + [ + xsd.Attribute( + etree.QName('http://tests.python-zeep.org/', 'attr_1'), + xsd.String()), + xsd.Attribute('attr_2', xsd.String()), + ] + )) + expected = load_xml(""" + + foo + bar + + """) + obj = custom_element.parse(expected, None) + assert obj.item_1 == 'foo' + assert obj.item_2 == 'bar' + assert obj.attr_1 == 'x' + assert obj.attr_2 == 'y' + + +def test_build_sequence_with_optional_elements(): + custom_type = xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'container'), + xsd.ComplexType( + xsd.Sequence([ + xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'item_1'), + xsd.String()), + xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'item_2'), + xsd.ComplexType( + xsd.Sequence([ + xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'item_2_1'), + xsd.String(), + nillable=True) + ]) + ) + ), + xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'item_3'), + xsd.String(), + max_occurs=2), + xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'item_4'), + xsd.String(), + min_occurs=0), + ]) + )) + expected = etree.fromstring(""" + + 1 + + 3 + + """) + obj = custom_type.parse(expected, None) + assert obj.item_1 == '1' + assert obj.item_2 is None + assert obj.item_3 == ['3'] + assert obj.item_4 is None + + +def test_build_max_occurs_unbounded(): + custom_type = xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'authentication'), + xsd.ComplexType( + xsd.Sequence([ + xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'item_1'), + xsd.String()), + xsd.Element( + etree.QName('http://tests.python-zeep.org/', 'item_2'), + xsd.String()), + ], max_occurs='unbounded') + )) + expected = etree.fromstring(""" + + foo + bar + + """) + obj = custom_type.parse(expected, None) + assert obj._value_1 == [ + { + 'item_1': 'foo', + 'item_2': 'bar', + } + ] + + +def test_xml_sequence_with_choice(): + schema = xsd.Schema(load_xml(""" + + + + + + + + + + + + + + """)) + + xml = load_xml(""" + + blabla + haha + + """) + + elm = schema.get_element('{http://tests.python-zeep.org/tst}container') + result = elm.parse(xml, schema) + assert result.item_1 == 'blabla' + assert result.item_3 == 'haha' + + +def test_xml_sequence_with_choice_max_occurs_2(): + schema = xsd.Schema(load_xml(""" + + + + + + + + + + + + + + + + """)) + + xml = load_xml(""" + + item-1-1 + item-1-2 + item-3 + + """) + + elm = schema.get_element('{http://tests.python-zeep.org/tst}container') + result = elm.parse(xml, schema) + assert result._value_1 == [ + {'item_1': 'item-1-1'}, + {'item_1': 'item-1-2'}, + ] + + assert result.item_3 == 'item-3' + + +def test_xml_sequence_with_choice_max_occurs_3(): + schema = xsd.Schema(load_xml(""" + + + + + + + + + + + + + + + + + """)) + + xml = load_xml(""" + + text-1 + text-2 + text-1 + text-2 + text-3 + text-4 + + """) + + elm = schema.get_element('{http://tests.python-zeep.org/tst}container') + result = elm.parse(xml, schema) + assert result._value_1 == [ + {'item_1': 'text-1', 'item_2': 'text-2'}, + {'item_1': 'text-1', 'item_2': 'text-2'}, + {'item_3': 'text-3'}, + ] + assert result.item_4 == 'text-4' + + +def test_xml_sequence_with_nil_element(): + schema = xsd.Schema(load_xml(""" + + + + + + + + + + """)) + + xml = load_xml(""" + + text-1 + text-2 + + text-4 + text-5 + + """) + + elm = schema.get_element('{http://tests.python-zeep.org/}container') + result = elm.parse(xml, schema) + assert result.item == [ + 'text-1', + 'text-2', + None, + 'text-4', + 'text-5', + ] + + +def test_xml_sequence_unbounded(): + schema = xsd.Schema(load_xml(""" + + + + + + + + + + + + + + + """)) + + elm_type = schema.get_type('{http://tests.python-zeep.org/}ValueListType') + + with pytest.raises(TypeError): + elm_type(Value='bla') + elm_type(_value_1={'Value': 'bla'}) + + +def test_xml_sequence_recover_from_missing_element(): + schema = xsd.Schema(load_xml(""" + + + + + + + + + + + """), strict=False) + + xml = load_xml(""" + + text-1 + text-3 + text-4 + + """) + elm_type = schema.get_type('{http://tests.python-zeep.org/}container') + result = elm_type.parse_xmlelement(xml, schema) + assert result.item_1 == 'text-1' + assert result.item_2 is None + assert result.item_3 == 'text-3' + assert result.item_4 == 'text-4' diff --git a/tests/test_xsd_integration.py b/tests/test_xsd_integration.py index b262b48..04d796f 100644 --- a/tests/test_xsd_integration.py +++ b/tests/test_xsd_integration.py @@ -3,11 +3,11 @@ import copy import pytest from lxml import etree -from tests.utils import assert_nodes_equal, load_xml +from tests.utils import assert_nodes_equal, load_xml, render_node from zeep import xsd -def test_complex_type_nested_wrong_type(): +def test_xml_complex_type_nested_wrong_type(): schema = xsd.Schema(load_xml(""" - """.strip()) - - schema = xsd.Schema(node) + """)) schema.set_ns_prefix('tns', 'http://tests.python-zeep.org/') address_type = schema.get_element('tns:Address') @@ -130,8 +128,8 @@ def test_array(): assert_nodes_equal(expected, node) -def test_complex_type_unbounded_one(): - node = etree.fromstring(""" +def test_xml_complex_type_unbounded_one(): + schema = xsd.Schema(load_xml(""" - """.strip()) - - schema = xsd.Schema(node) + """)) address_type = schema.get_element('{http://tests.python-zeep.org/}Address') obj = address_type(foo=['foo']) @@ -164,8 +160,8 @@ def test_complex_type_unbounded_one(): assert_nodes_equal(expected, node) -def test_complex_type_unbounded_named(): - node = etree.fromstring(""" +def test_xml_complex_type_unbounded_named(): + schema = xsd.Schema(load_xml(""" - """.strip()) + """)) - schema = xsd.Schema(node) address_type = schema.get_element('{http://tests.python-zeep.org/}Address') obj = address_type() assert obj.foo == [] @@ -201,8 +196,8 @@ def test_complex_type_unbounded_named(): assert_nodes_equal(expected, node) -def test_complex_type_array_to_other_complex_object(): - node = etree.fromstring(""" +def test_xml_complex_type_array_to_other_complex_object(): + schema = xsd.Schema(load_xml(""" @@ -217,9 +212,8 @@ def test_complex_type_array_to_other_complex_object(): - """.strip()) # noqa + """)) - schema = xsd.Schema(node) address_array = schema.get_element('ArrayOfAddress') obj = address_array() assert obj.Address == [] @@ -227,21 +221,26 @@ def test_complex_type_array_to_other_complex_object(): obj.Address.append(schema.get_type('Address')(foo='foo')) obj.Address.append(schema.get_type('Address')(foo='bar')) - node = etree.fromstring(""" + expected = """ - -
- foo -
-
- bar -
-
- """.strip()) + + +
+ foo +
+
+ bar +
+
+
+ """ + + result = render_node(address_array, obj) + assert_nodes_equal(expected, result) -def test_complex_type_init_kwargs(): - node = etree.fromstring(""" +def test_xml_complex_type_init_kwargs(): + schema = xsd.Schema(load_xml(""" - """.strip()) + """)) - schema = xsd.Schema(node) address_type = schema.get_element('{http://tests.python-zeep.org/}Address') obj = address_type( NameFirst='John', NameLast='Doe', Email='j.doe@example.com') @@ -267,8 +265,8 @@ def test_complex_type_init_kwargs(): assert obj.Email == 'j.doe@example.com' -def test_complex_type_init_args(): - node = etree.fromstring(""" +def test_xml_complex_type_init_args(): + schema = xsd.Schema(load_xml(""" - """.strip()) - - schema = xsd.Schema(node) + """)) address_type = schema.get_element('{http://tests.python-zeep.org/}Address') obj = address_type('John', 'Doe', 'j.doe@example.com') assert obj.NameFirst == 'John' @@ -293,107 +289,9 @@ def test_complex_type_init_args(): assert obj.Email == 'j.doe@example.com' -def test_group(): - node = etree.fromstring(""" - - - - - - - - - - - - - - - - - - """.strip()) - schema = xsd.Schema(node) - address_type = schema.get_element('{http://tests.python-zeep.org/}Address') - - obj = address_type(first_name='foo', last_name='bar') - - node = etree.Element('document') - address_type.render(node, obj) - expected = """ - - - foo - bar - - - """ - assert_nodes_equal(expected, node) - - -def test_group_for_type(): - node = etree.fromstring(""" - - - - - - - - - - - - - - - - - - - - - - blub - - - - - - - - """.strip()) - schema = xsd.Schema(node) - address_type = schema.get_element('{http://tests.python-zeep.org/}Address') - - obj = address_type( - first_name='foo', last_name='bar', - city='Utrecht', country='The Netherlands') - - node = etree.Element('document') - address_type.render(node, obj) - expected = """ - - - foo - bar - Utrecht - The Netherlands - - - """ - assert_nodes_equal(expected, node) - - -def test_element_ref_missing_namespace(): +def test_xml_element_ref_missing_namespace(): # For buggy soap servers (#170) - node = etree.fromstring(""" + schema = xsd.Schema(load_xml(""" - """.strip()) - - schema = xsd.Schema(node) + """)) custom_type = schema.get_element('{http://tests.python-zeep.org/}bar') input_xml = load_xml(""" @@ -421,8 +317,8 @@ def test_element_ref_missing_namespace(): assert item.foo == 'bar' -def test_element_ref(): - node = etree.fromstring(""" +def test_xml_element_ref(): + schema = xsd.Schema(load_xml(""" - """.strip()) - - schema = xsd.Schema(node) + """)) foo_type = schema.get_element('{http://tests.python-zeep.org/}foo') assert isinstance(foo_type.type, xsd.String) @@ -460,8 +354,8 @@ def test_element_ref(): assert_nodes_equal(expected, node) -def test_element_ref_occurs(): - node = etree.fromstring(""" +def test_xml_element_ref_occurs(): + schema = xsd.Schema(load_xml(""" - """.strip()) - - schema = xsd.Schema(node) + """)) foo_type = schema.get_element('{http://tests.python-zeep.org/}foo') assert isinstance(foo_type.type, xsd.String) @@ -500,8 +392,8 @@ def test_element_ref_occurs(): assert_nodes_equal(expected, node) -def test_unqualified(): - node = etree.fromstring(""" +def test_xml_unqualified(): + schema = xsd.Schema(load_xml(""" - """.strip()) + """)) - schema = xsd.Schema(node) address_type = schema.get_element('{http://tests.python-zeep.org/}Address') obj = address_type(foo='bar') @@ -536,8 +427,8 @@ def test_unqualified(): assert_nodes_equal(expected, node) -def test_defaults(): - node = etree.fromstring(""" +def test_xml_defaults(): + schema = xsd.Schema(load_xml(""" - + + - + - """.strip()) + """)) - schema = xsd.Schema(node) container_type = schema.get_element( '{http://tests.python-zeep.org/}container') obj = container_type() - assert obj.foo == "hoi" - assert obj.bar == "hoi" + assert obj.item_1 == "hoi" + assert obj.item_2 is None + assert obj.attr_1 == "hoi" expected = """ - - hoi + + hoi + + + """ + node = etree.Element('document') + container_type.render(node, obj) + assert_nodes_equal(expected, node) + + obj.item_2 = 'ok' + + expected = """ + + + hoi + ok """ @@ -574,8 +480,8 @@ def test_defaults(): assert_nodes_equal(expected, node) -def test_defaults_parse(): - node = etree.fromstring(""" +def test_xml_defaults_parse_boolean(): + schema = xsd.Schema(load_xml(""" - + - + - """.strip()) + """)) + + container_type = schema.get_element( + '{http://tests.python-zeep.org/}container') + obj = container_type() + assert obj.foo == "false" + assert obj.bar == "0" + + expected = """ + + + false + + + """ + node = etree.Element('document') + container_type.render(node, obj) + assert_nodes_equal(expected, node) + + +def test_xml_defaults_parse(): + schema = xsd.Schema(load_xml(""" + + + + + + + + + + + + """)) - schema = xsd.Schema(node) container_elm = schema.get_element( '{http://tests.python-zeep.org/}container') node = load_xml(""" - hoi + hoi """) item = container_elm.parse(node, schema) - assert item.bar == 'hoi' + assert item.attr_1 == 'hoi' -def test_init_with_dicts(): - node = etree.fromstring(""" +def test_xml_init_with_dicts(): + schema = xsd.Schema(load_xml(""" - """.strip()) + """)) - schema = xsd.Schema(node) address_type = schema.get_element('{http://tests.python-zeep.org/}Address') obj = address_type(name='foo', container={'service': [{'name': 'foo'}]}) @@ -662,9 +603,8 @@ def test_init_with_dicts(): assert_nodes_equal(expected, node) - -def test_sequence_in_sequence(): - node = load_xml(""" +def test_xml_sequence_in_sequence(): + schema = xsd.Schema(load_xml(""" - """) - schema = xsd.Schema(node) + """)) element = schema.get_element('ns0:container') value = element(item_1="foo", item_2="bar") @@ -703,7 +642,7 @@ def test_sequence_in_sequence(): assert_nodes_equal(expected, node) -def test_sequence_in_sequence_many(): +def test_xml_sequence_in_sequence_many(): node = load_xml(""" - """.strip()) - - schema = xsd.Schema(node) + """)) container_elm = schema.get_element('{http://tests.python-zeep.org/}container') obj = container_elm(something={}) @@ -790,7 +727,7 @@ def test_complex_type_empty(): assert item.something is None -def test_schema_as_payload(): +def test_xml_schema_as_payload(): schema = xsd.Schema(load_xml(""" - """.strip()) - - schema = xsd.Schema(node) + """)) container_elm = schema.get_element('{http://tests.python-zeep.org/}container') node = load_xml(""" @@ -905,8 +840,8 @@ def test_empty_xmlns(): assert item._value_1 == 'foo' -def test_keep_objects_intact(): - node = etree.fromstring(""" +def test_xml_keep_objects_intact(): + schema = xsd.Schema(load_xml(""" - """.strip()) + """)) - schema = xsd.Schema(node) address_type = schema.get_element('{http://tests.python-zeep.org/}Address') obj = address_type(name='foo', container={'service': [{'name': 'foo'}]}) diff --git a/tests/test_xsd_parse.py b/tests/test_xsd_parse.py index aa0e76c..511d019 100644 --- a/tests/test_xsd_parse.py +++ b/tests/test_xsd_parse.py @@ -7,132 +7,6 @@ from zeep import xsd from zeep.xsd.schema import Schema -def test_sequence_parse_basic(): - custom_type = xsd.Element( - etree.QName('http://tests.python-zeep.org/', 'authentication'), - xsd.ComplexType( - xsd.Sequence([ - xsd.Element( - etree.QName('http://tests.python-zeep.org/', 'item_1'), - xsd.String()), - xsd.Element( - etree.QName('http://tests.python-zeep.org/', 'item_2'), - xsd.String()), - ]) - )) - expected = etree.fromstring(""" - - foo - bar - - """) - obj = custom_type.parse(expected, None) - assert obj.item_1 == 'foo' - assert obj.item_2 == 'bar' - - -def test_sequence_parse_max_occurs_infinite_loop(): - custom_type = xsd.Element( - etree.QName('http://tests.python-zeep.org/', 'authentication'), - xsd.ComplexType( - xsd.Sequence([ - xsd.Element( - etree.QName('http://tests.python-zeep.org/', 'item_1'), - xsd.String()), - xsd.Element( - etree.QName('http://tests.python-zeep.org/', 'item_2'), - xsd.String()), - ], max_occurs='unbounded') - )) - expected = etree.fromstring(""" - - foo - bar - - """) - obj = custom_type.parse(expected, None) - assert obj._value_1 == [ - { - 'item_1': 'foo', - 'item_2': 'bar', - } - ] - -def test_sequence_parse_basic_with_attrs(): - custom_element = xsd.Element( - etree.QName('http://tests.python-zeep.org/', 'authentication'), - xsd.ComplexType( - xsd.Sequence([ - xsd.Element( - etree.QName('http://tests.python-zeep.org/', 'item_1'), - xsd.String()), - xsd.Element( - etree.QName('http://tests.python-zeep.org/', 'item_2'), - xsd.String()), - ]), - [ - xsd.Attribute( - etree.QName('http://tests.python-zeep.org/', 'attr_1'), - xsd.String()), - xsd.Attribute('attr_2', xsd.String()), - ] - )) - expected = etree.fromstring(""" - - foo - bar - - """) - obj = custom_element.parse(expected, None) - assert obj.item_1 == 'foo' - assert obj.item_2 == 'bar' - assert obj.attr_1 == 'x' - assert obj.attr_2 == 'y' - - -def test_sequence_parse_with_optional(): - custom_type = xsd.Element( - etree.QName('http://tests.python-zeep.org/', 'container'), - xsd.ComplexType( - xsd.Sequence([ - xsd.Element( - etree.QName('http://tests.python-zeep.org/', 'item_1'), - xsd.String()), - xsd.Element( - etree.QName('http://tests.python-zeep.org/', 'item_2'), - xsd.ComplexType( - xsd.Sequence([ - xsd.Element( - etree.QName('http://tests.python-zeep.org/', 'item_2_1'), - xsd.String(), - nillable=True) - ]) - ) - ), - xsd.Element( - etree.QName('http://tests.python-zeep.org/', 'item_3'), - xsd.String(), - max_occurs=2), - xsd.Element( - etree.QName('http://tests.python-zeep.org/', 'item_4'), - xsd.String(), - min_occurs=0), - ]) - )) - expected = etree.fromstring(""" - - 1 - - 3 - - """) - obj = custom_type.parse(expected, None) - assert obj.item_1 == '1' - assert obj.item_2 is None - assert obj.item_3 == ['3'] - assert obj.item_4 is None - - def test_sequence_parse_regression(): schema_doc = load_xml(b""" @@ -239,7 +113,7 @@ def test_sequence_parse_anytype_obj(): '{http://www.w3.org/2001/XMLSchema}Schema', targetNamespace='http://tests.python-zeep.org/')) - root = next(schema.documents) + root = schema.root_document root.register_type('{http://tests.python-zeep.org/}something', value_type) custom_type = xsd.Element( @@ -264,147 +138,6 @@ def test_sequence_parse_anytype_obj(): assert obj.item_1.value == 100 -def test_sequence_parse_choice(): - schema_doc = load_xml(b""" - - - - - - - - - - - - - - - """) - - xml = load_xml(b""" - - - blabla - haha - - """) - - schema = xsd.Schema(schema_doc) - elm = schema.get_element('{http://tests.python-zeep.org/tst}container') - result = elm.parse(xml, schema) - assert result.item_1 == 'blabla' - assert result.item_3 == 'haha' - - -def test_sequence_parse_choice_max_occurs(): - schema_doc = load_xml(b""" - - - - - - - - - - - - - - - - - """) - - xml = load_xml(b""" - - - item-1-1 - item-1-2 - item-3 - - """) - - schema = xsd.Schema(schema_doc) - elm = schema.get_element('{http://tests.python-zeep.org/tst}container') - result = elm.parse(xml, schema) - assert result._value_1 == [ - {'item_1': 'item-1-1'}, - {'item_1': 'item-1-2'}, - ] - - assert result.item_3 == 'item-3' - - -def test_sequence_parse_choice_sequence_max_occurs(): - schema_doc = load_xml(b""" - - - - - - - - - - - - - - - - - - """) - - xml = load_xml(b""" - - - text-1 - text-2 - text-1 - text-2 - text-3 - text-4 - - """) - - schema = xsd.Schema(schema_doc) - elm = schema.get_element('{http://tests.python-zeep.org/tst}container') - result = elm.parse(xml, schema) - assert result._value_1 == [ - {'item_1': 'text-1', 'item_2': 'text-2'}, - {'item_1': 'text-1', 'item_2': 'text-2'}, - {'item_3': 'text-3'}, - ] - assert result.item_4 == 'text-4' - - def test_sequence_parse_anytype_regression_17(): schema_doc = load_xml(b""" @@ -465,177 +198,6 @@ def test_sequence_parse_anytype_regression_17(): assert result.getCustomFieldReturn.value.content == 'Test Solution' -def test_sequence_min_occurs_2(): - custom_type = xsd.Element( - etree.QName('http://tests.python-zeep.org/', 'authentication'), - xsd.ComplexType( - xsd.Sequence([ - xsd.Element( - etree.QName('http://tests.python-zeep.org/', 'item_1'), - xsd.String()), - xsd.Element( - etree.QName('http://tests.python-zeep.org/', 'item_2'), - xsd.String()), - ], min_occurs=2, max_occurs=2) - )) - - # INIT - elm = custom_type(_value_1=[ - {'item_1': 'foo-1', 'item_2': 'bar-1'}, - {'item_1': 'foo-2', 'item_2': 'bar-2'}, - ]) - - assert elm._value_1 == [ - {'item_1': 'foo-1', 'item_2': 'bar-1'}, - {'item_1': 'foo-2', 'item_2': 'bar-2'}, - ] - - expected = etree.fromstring(""" - - foo - bar - foo - bar - - """) - obj = custom_type.parse(expected, None) - assert obj._value_1 == [ - { - 'item_1': 'foo', - 'item_2': 'bar', - }, - { - 'item_1': 'foo', - 'item_2': 'bar', - }, - ] - - -def test_all_basic(): - custom_type = xsd.Element( - etree.QName('http://tests.python-zeep.org/', 'authentication'), - xsd.ComplexType( - xsd.All([ - xsd.Element( - etree.QName('http://tests.python-zeep.org/', 'item_1'), - xsd.String()), - xsd.Element( - etree.QName('http://tests.python-zeep.org/', 'item_2'), - xsd.String()), - ]) - )) - expected = etree.fromstring(""" - - bar - foo - - """) - obj = custom_type.parse(expected, None) - assert obj.item_1 == 'foo' - assert obj.item_2 == 'bar' - - -def test_group_optional(): - custom_type = xsd.Element( - etree.QName('http://tests.python-zeep.org/', 'authentication'), - xsd.ComplexType( - xsd.Group( - etree.QName('http://tests.python-zeep.org/', 'foobar'), - xsd.Sequence([ - xsd.Element( - etree.QName('http://tests.python-zeep.org/', 'item_1'), - xsd.String()), - xsd.Element( - etree.QName('http://tests.python-zeep.org/', 'item_2'), - xsd.String()), - ]), - min_occurs=1) - )) - expected = etree.fromstring(""" - - foo - bar - - """) - obj = custom_type.parse(expected, None) - assert obj.item_1 == 'foo' - assert obj.item_2 == 'bar' - assert not hasattr(obj, 'foobar') - - -def test_group_min_occurs_2(): - custom_type = xsd.Element( - etree.QName('http://tests.python-zeep.org/', 'authentication'), - xsd.ComplexType( - xsd.Group( - etree.QName('http://tests.python-zeep.org/', 'foobar'), - xsd.Sequence([ - xsd.Element( - etree.QName('http://tests.python-zeep.org/', 'item_1'), - xsd.String()), - xsd.Element( - etree.QName('http://tests.python-zeep.org/', 'item_2'), - xsd.String()), - ]), - min_occurs=2, max_occurs=2) - )) - expected = etree.fromstring(""" - - foo - bar - foo - bar - - """) - obj = custom_type.parse(expected, None) - assert obj._value_1 == [ - {'item_1': 'foo', 'item_2': 'bar'}, - {'item_1': 'foo', 'item_2': 'bar'}, - ] - assert not hasattr(obj, 'foobar') - - -def test_group_min_occurs_2_sequence_min_occurs_2(): - custom_type = xsd.Element( - etree.QName('http://tests.python-zeep.org/', 'authentication'), - xsd.ComplexType( - xsd.Group( - etree.QName('http://tests.python-zeep.org/', 'foobar'), - xsd.Sequence([ - xsd.Element( - etree.QName('http://tests.python-zeep.org/', 'item_1'), - xsd.String()), - xsd.Element( - etree.QName('http://tests.python-zeep.org/', 'item_2'), - xsd.String()), - ], min_occurs=2, max_occurs=2), - min_occurs=2, max_occurs=2) - )) - expected = etree.fromstring(""" - - foo - bar - foo - bar - foo - bar - foo - bar - - """) - obj = custom_type.parse(expected, None) - assert obj._value_1 == [ - {'_value_1': [ - {'item_1': 'foo', 'item_2': 'bar'}, - {'item_1': 'foo', 'item_2': 'bar'}, - ]}, - {'_value_1': [ - {'item_1': 'foo', 'item_2': 'bar'}, - {'item_1': 'foo', 'item_2': 'bar'}, - ]}, - ] - assert not hasattr(obj, 'foobar') - def test_nested_complex_type(): custom_type = xsd.Element( @@ -723,7 +285,7 @@ def test_nested_complex_type_optional(): """) obj = custom_type.parse(expected, None) assert obj.item_1 == 'foo' - assert obj.item_2 == [] + assert obj.item_2 == [None] expected = etree.fromstring(""" @@ -885,3 +447,19 @@ def test_parse_invalid_values(): assert result.item_2 == datetime.date(2016, 10, 20) assert result.attr_1 is None assert result.attr_2 == datetime.date(2013, 10, 20) + + +def test_xsd_missing_localname(): + schema = xsd.Schema(load_xml(b""" + + + + + """)) + + schema.get_element('{http://tests.python-zeep.org/}container') diff --git a/tests/test_xsd_schemas.py b/tests/test_xsd_schemas.py index a629e46..44a7a96 100644 --- a/tests/test_xsd_schemas.py +++ b/tests/test_xsd_schemas.py @@ -4,6 +4,8 @@ from lxml import etree from tests.utils import DummyTransport, load_xml from zeep import exceptions, xsd from zeep.xsd import Schema +from zeep.xsd.types.unresolved import UnresolvedType +from tests.utils import assert_nodes_equal, load_xml, render_node def test_default_types(): @@ -98,7 +100,7 @@ def test_invalid_localname_handling(): def test_schema_repr_none(): schema = xsd.Schema() - assert repr(schema) == "')>" + assert repr(schema) == "" def test_schema_repr_val(): @@ -111,7 +113,7 @@ def test_schema_repr_val(): elementFormDefault="qualified"> """)) - assert repr(schema) == "" + assert repr(schema) == "" def test_schema_doc_repr_val(): @@ -433,8 +435,8 @@ def test_duplicate_target_namespace(): elm_b = schema.get_element('{http://tests.python-zeep.org/duplicate}elm-in-b') elm_c = schema.get_element('{http://tests.python-zeep.org/duplicate}elm-in-c') - assert not isinstance(elm_b.type, xsd.UnresolvedType) - assert not isinstance(elm_c.type, xsd.UnresolvedType) + assert not isinstance(elm_b.type, UnresolvedType) + assert not isinstance(elm_c.type, UnresolvedType) def test_multiple_no_namespace(): @@ -644,6 +646,136 @@ def test_include_recursion(): schema.get_element('{http://tests.python-zeep.org/b}bar') +def test_include_relative(): + node_a = etree.fromstring(""" + + + + + + + """.strip()) + + node_b = etree.fromstring(""" + + + + + + """.strip()) + + node_c = etree.fromstring(""" + + + + + """.strip()) + + transport = DummyTransport() + transport.bind('http://tests.python-zeep.org/subdir/b.xsd', node_b) + transport.bind('http://tests.python-zeep.org/subdir/c.xsd', node_c) + + schema = xsd.Schema(node_a, transport=transport) + schema.get_element('{http://tests.python-zeep.org/a}foo') + schema.get_element('{http://tests.python-zeep.org/a}bar') + + +def test_include_no_default_namespace(): + node_a = etree.fromstring(""" + + + + + + + + """.strip()) + + # include without default namespace, other xsd prefix + node_b = etree.fromstring(""" + + + + + + + + + + + + + + """.strip()) + + transport = DummyTransport() + transport.bind('http://tests.python-zeep.org/b.xsd', node_b) + schema = xsd.Schema(node_a, transport=transport) + item = schema.get_element('{http://tests.python-zeep.org/tns}container') + assert item + + +def test_include_different_form_defaults(): + node_a = etree.fromstring(""" + + + + + + """.strip()) + + # include without default namespace, other xsd prefix + node_b = load_xml(""" + + + + + + + + + + + + + """) + + transport = DummyTransport() + transport.bind('http://tests.python-zeep.org/b.xsd', node_b) + + schema = xsd.Schema(node_a, transport=transport) + item = schema.get_element('{http://tests.python-zeep.org/}container') + obj = item(item='foo', attr='bar') + node = render_node(item, obj) + + expected = load_xml(""" + + + foo + + + """) + assert_nodes_equal(expected, node) + def test_merge(): node_a = etree.fromstring(""" @@ -674,3 +806,37 @@ def test_merge(): schema_a.get_element('{http://tests.python-zeep.org/a}foo') schema_a.get_element('{http://tests.python-zeep.org/b}foo') + + +def test_xml_namespace(): + xmlns = load_xml(""" + + + + + """) + + transport = DummyTransport() + transport.bind('http://www.w3.org/2001/xml.xsd', xmlns) + + xsd.Schema(load_xml(""" + + + + + + + + + + + """), transport=transport) diff --git a/tests/test_xsd_signatures.py b/tests/test_xsd_signatures.py index 0040d6a..1bcf48e 100644 --- a/tests/test_xsd_signatures.py +++ b/tests/test_xsd_signatures.py @@ -1,6 +1,7 @@ from lxml import etree from zeep import xsd +from tests.utils import load_xml def test_signature_complex_type_choice(): @@ -16,7 +17,7 @@ def test_signature_complex_type_choice(): xsd.String()), ]) )) - assert custom_type.signature() == '({item_1: xsd:string} | {item_2: xsd:string})' + assert custom_type.signature() == '{http://tests.python-zeep.org/}authentication(({item_1: xsd:string} | {item_2: xsd:string}))' def test_signature_complex_type_choice_sequence(): @@ -38,7 +39,7 @@ def test_signature_complex_type_choice_sequence(): ]) )) assert custom_type.signature() == ( - '({item_1: xsd:string} | {item_2_1: xsd:string, item_2_2: xsd:string})') + '{http://tests.python-zeep.org/}authentication(({item_1: xsd:string} | {item_2_1: xsd:string, item_2_2: xsd:string}))') def test_signature_nested_sequences(): @@ -80,7 +81,7 @@ def test_signature_nested_sequences(): )) assert custom_type.signature() == ( - 'item_1: xsd:string, item_2: xsd:string, item_3: xsd:string, item_4: xsd:string, ({item_5: xsd:string} | {item_6: xsd:string} | {item_5: xsd:string, item_6: xsd:string})' # noqa + '{http://tests.python-zeep.org/}authentication(item_1: xsd:string, item_2: xsd:string, item_3: xsd:string, item_4: xsd:string, ({item_5: xsd:string} | {item_6: xsd:string} | {item_5: xsd:string, item_6: xsd:string}))' ) @@ -123,7 +124,7 @@ def test_signature_nested_sequences_multiple(): )) assert custom_type.signature() == ( - 'item_1: xsd:string, item_2: xsd:string, item_3: xsd:string, item_4: xsd:string, _value_1: ({item_5: xsd:string} | {item_6: xsd:string} | {item_5: xsd:string, item_6: xsd:string})[]' # noqa + '{http://tests.python-zeep.org/}authentication(item_1: xsd:string, item_2: xsd:string, item_3: xsd:string, item_4: xsd:string, ({item_5: xsd:string} | {item_6: xsd:string} | {item_5: xsd:string, item_6: xsd:string})[])' ) @@ -138,7 +139,7 @@ def test_signature_complex_type_any(): xsd.Any() ]) )) - assert custom_type.signature() == '({item_1: xsd:string} | {_value_1: ANY})' + assert custom_type.signature() == '{http://tests.python-zeep.org/}authentication(({item_1: xsd:string} | {_value_1: ANY}))' custom_type(item_1='foo') @@ -161,7 +162,7 @@ def test_signature_complex_type_sequence_with_any(): ]) )) assert custom_type.signature() == ( - '({item_1: xsd:string} | {item_2: {_value_1: ANY}})') + '{http://tests.python-zeep.org/}authentication(({item_1: xsd:string} | {item_2: {_value_1: ANY}}))') def test_signature_complex_type_sequence_with_anys(): @@ -184,4 +185,30 @@ def test_signature_complex_type_sequence_with_anys(): ]) )) assert custom_type.signature() == ( - '({item_1: xsd:string} | {item_2: {_value_1: ANY, _value_2: ANY}})') + '{http://tests.python-zeep.org/}authentication(' + + '({item_1: xsd:string} | {item_2: {_value_1: ANY, _value_2: ANY}})' + + ')') + +def test_schema_recursive_ref(): + schema = xsd.Schema(load_xml(""" + + + + + + + + + + + + + """)) + + elm = schema.get_element('ns0:Container') + elm.signature(schema) + diff --git a/tests/test_xsd_union.py b/tests/test_xsd_union.py index 1993ff5..679ba14 100644 --- a/tests/test_xsd_union.py +++ b/tests/test_xsd_union.py @@ -6,6 +6,7 @@ def test_union_same_types(): schema = xsd.Schema(load_xml(""" - + @@ -49,7 +50,7 @@ def test_union_mixed(): elementFormDefault="qualified"> - + diff --git a/tests/test_xsd_valueobjects.py b/tests/test_xsd_valueobjects.py index f9c6eaa..5874f0f 100644 --- a/tests/test_xsd_valueobjects.py +++ b/tests/test_xsd_valueobjects.py @@ -1,5 +1,9 @@ +import json +import pickle + import pytest import six +from lxml.etree import QName from zeep import xsd from zeep.xsd import valueobjects @@ -214,9 +218,10 @@ def test_choice_mixed(): xsd.Element('item_2', xsd.String()), ]), xsd.Element('item_2', xsd.String()) - ]) + ]), + qname=QName('http://tests.python-zeep.org', 'container') ) - expected = '({item_1: xsd:string} | {item_2: xsd:string}), item_2__1: xsd:string' + expected = '{http://tests.python-zeep.org}container(({item_1: xsd:string} | {item_2: xsd:string}), item_2__1: xsd:string)' assert xsd_type.signature() == expected args = tuple([]) @@ -401,3 +406,35 @@ def test_choice_sequences_init_dict(): {'item_1': 'value-1', 'item_2': 'value-2'} ] } + + +def test_pickle(): + xsd_type = xsd.ComplexType( + xsd.Sequence([ + xsd.Element('item_1', xsd.String()), + xsd.Element('item_2', xsd.String()) + ])) + + obj = xsd_type(item_1='x', item_2='y') + + data = pickle.dumps(obj) + obj_rt = pickle.loads(data) + + assert obj.item_1 == 'x' + assert obj.item_2 == 'y' + assert obj_rt.item_1 == 'x' + assert obj_rt.item_2 == 'y' + + +def test_json(): + xsd_type = xsd.ComplexType( + xsd.Sequence([ + xsd.Element('item_1', xsd.String()), + xsd.Element('item_2', xsd.String()) + ])) + + obj = xsd_type(item_1='x', item_2='y') + assert obj.__json__() == { + 'item_1': 'x', + 'item_2': 'y', + } diff --git a/tests/test_xsd_visitor.py b/tests/test_xsd_visitor.py index 66d561b..d5e343e 100644 --- a/tests/test_xsd_visitor.py +++ b/tests/test_xsd_visitor.py @@ -23,7 +23,7 @@ def test_schema_empty(): """) schema = parse_schema_node(node) - root = next(schema.documents) + root = schema._get_schema_documents('http://tests.python-zeep.org/')[0] assert root._element_form == 'qualified' assert root._attribute_form == 'unqualified' diff --git a/tests/wsdl_files/soap.wsdl b/tests/wsdl_files/soap.wsdl index ab28e67..5783270 100644 --- a/tests/wsdl_files/soap.wsdl +++ b/tests/wsdl_files/soap.wsdl @@ -1,13 +1,13 @@ - - @@ -17,6 +17,12 @@ + + + + + + @@ -87,6 +93,9 @@ + + + @@ -105,6 +114,12 @@ + + + + + + My first service diff --git a/tests/wsdl_files/soap_header.wsdl b/tests/wsdl_files/soap_header.wsdl index 04e1269..aa1e169 100644 --- a/tests/wsdl_files/soap_header.wsdl +++ b/tests/wsdl_files/soap_header.wsdl @@ -1,7 +1,16 @@ - + - + @@ -48,6 +57,10 @@ + + + + @@ -65,6 +78,7 @@ +